JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 38
  • Score
    100M100P100Q70328F
  • License MIT

Database-backed notes system with file attachment support for the hazo ecosystem

Package Exports

  • hazo_notes
  • hazo_notes/api
  • hazo_notes/components
  • hazo_notes/hooks
  • hazo_notes/lib
  • hazo_notes/types

Readme

hazo_notes

Database-backed notes system with file attachment support for Next.js applications in the hazo ecosystem.

Features

  • Database-backed persistence - Notes stored in PostgreSQL or SQLite via hazo_connect
  • File attachments - Support for images, PDFs, and documents with embed/attach modes
  • Flexible UI styles - Choose between popover or slide panel presentation
  • Smart save modes - Explicit save/cancel buttons or auto-save on blur
  • INI-based configuration - Simple config file for all settings
  • Full TypeScript support - Complete type definitions included
  • Controlled and uncontrolled modes - Works with parent state or manages its own
  • Paste-to-embed images - Paste images directly into notes
  • User attribution - Automatic user profiles with avatars (optional)
  • File reference syntax - Inline file references with <<embed:XXXX>> and <<attach:XXXX>>

Prerequisites

Before installing hazo_notes, ensure you have:

  • Next.js 14+ with React 18+
  • Tailwind CSS configured
  • hazo_connect installed and configured (for database access)
  • PostgreSQL or SQLite database

Installation

npm install hazo_notes

Peer Dependencies

Install these based on your needs:

# Recommended for full functionality
npm install hazo_connect hazo_auth hazo_logs react-icons

Note: Radix UI primitives (@radix-ui/react-popover and @radix-ui/react-dialog) are bundled with this package. You don't need to install them separately.

Quick Start

1. Add the Component

import { HazoNotesIcon } from 'hazo_notes';

function MyComponent() {
  return (
    <div className="flex items-center gap-2">
      <h2>Customer Information</h2>
      <HazoNotesIcon
        ref_id="customer-info-section"
        label="Customer Information"
      />
    </div>
  );
}

The component bundles its own Radix UI primitives, so no additional UI component setup is required.

2. Create API Route

Create app/api/hazo_notes/[ref_id]/route.ts:

import { createNotesHandler } from 'hazo_notes/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { hazo_get_tenant_auth } from 'hazo_auth/server-lib';
import { getUserById } from '@/lib/users'; // Replace with your user lookup
import type { NextRequest } from 'next/server';

export const dynamic = 'force-dynamic';

const { GET, POST } = createNotesHandler({
  getHazoConnect: () => getHazoConnectSingleton(),
  // Preferred: hazo_auth@6.x tenant auth. Used by both GET and POST.
  getAuth: async (req) => {
    const auth = await hazo_get_tenant_auth(req as NextRequest);
    if (!auth.authenticated || !auth.user?.id) return null;
    return {
      user_id: auth.user.id,
      scope_id: auth.selected_scope_id ?? null,
    };
  },
  getUserProfile: async (userId) => {
    const user = await getUserById(userId);
    return {
      id: userId,
      name: user?.name || 'Unknown User',
      email: user?.email || '',
      profile_image: user?.avatar,
    };
  },
});

export { GET, POST };

GET requires auth. Starting in 1.2.0, GET responds 401 when the auth resolver returns null, matching POST. Older releases left GET anonymous. If you're still on hazo_auth@5.x, use getUserIdFromRequest instead of getAuth — the package falls back to it for backward compatibility.

Error detail. On 500 responses the underlying error message is included as cause in dev (when NODE_ENV !== 'production'). Set expose_error_cause: false to suppress, or true to force it on in prod.

3. Set Up Database

Run the migration:

PostgreSQL:

CREATE TABLE hazo_notes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ref_id UUID NOT NULL,
  note JSONB NOT NULL DEFAULT '[]'::jsonb,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  changed_at TIMESTAMPTZ,
  note_count INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX idx_hazo_notes_ref_id ON hazo_notes(ref_id);

-- REQUIRED for PostgREST consumers — without this, GET/POST return 500 with
-- cause "permission denied for table hazo_notes" (PostgREST error 42501).
-- Shipped as migrations/002_grant_api_user_hazo_notes.sql.
GRANT SELECT, INSERT, UPDATE, DELETE ON hazo_notes TO api_user;

SQLite:

CREATE TABLE IF NOT EXISTS hazo_notes (
  id TEXT PRIMARY KEY,
  ref_id TEXT NOT NULL,
  note TEXT NOT NULL DEFAULT '[]',
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  changed_at TEXT,
  note_count INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_hazo_notes_ref_id ON hazo_notes(ref_id);

4. Configure (Optional)

Copy the config template:

mkdir -p config
cp node_modules/hazo_notes/templates/config/hazo_notes_config.ini config/

Edit config/hazo_notes_config.ini to customize behavior.

For detailed setup instructions, see SETUP_CHECKLIST.md.

Usage Examples

Basic Usage

import { HazoNotesIcon } from 'hazo_notes';

export default function FormPage() {
  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <label>Annual Income</label>
        <HazoNotesIcon
          ref_id="income-field"
          label="Annual Income"
        />
      </div>
      <input type="number" name="income" />
    </div>
  );
}

Result: Click the notes icon to add contextual notes about this field.

With File Attachments

<HazoNotesIcon
  ref_id="contract-review"
  label="Contract Review"
  enable_files={true}
  max_files_per_note={5}
  allowed_file_types={['pdf', 'docx', 'png', 'jpg']}
  max_file_size_mb={10}
/>

Features:

  • Upload files via file picker or paste images
  • Files referenced in note text with <<embed:0001>> or <<attach:0001>>
  • Images display inline, other files show as download links

Slide Panel Style

<HazoNotesIcon
  ref_id="detailed-notes"
  label="Detailed Notes"
  panel_style="slide_panel"
/>

Result: Notes open in a slide-out panel instead of a popover.

Auto-Save Mode

<HazoNotesIcon
  ref_id="quick-notes"
  label="Quick Notes"
  save_mode="auto"
/>

Result: Notes save automatically when panel closes (no save/cancel buttons).

Custom Styling

<HazoNotesIcon
  ref_id="styled-notes"
  label="Styled Notes"
  background_color="bg-blue-100"
  icon_size={24}
  show_border={false}
  className="ml-2"
/>

Supported color themes: yellow (default), amber, green, blue, red, orange, purple, pink, teal, gray. The background_color prop accepts any Tailwind bg-{color}-{shade} class — matching colors from the supported list will apply a full coordinated theme (header, text, borders, buttons). Unknown colors fall back to yellow.

{/* Blue-themed notes */}
<HazoNotesIcon ref_id="blue-notes" background_color="bg-blue-100" />

{/* Green-themed notes */}
<HazoNotesIcon ref_id="green-notes" background_color="bg-green-100" />

Controlled Mode (Notes State)

'use client';

import { useState } from 'react';
import { HazoNotesIcon } from 'hazo_notes';
import type { NoteEntry } from 'hazo_notes/types';

export default function ControlledNotesExample() {
  const [notes, setNotes] = useState<NoteEntry[]>([]);

  return (
    <HazoNotesIcon
      ref_id="controlled-notes"
      label="Controlled Notes"
      notes={notes}
      on_notes_change={setNotes}
    />
  );
}

Use Case: Sync notes with parent component state or external state management.

Controlled Mode (Open State)

Control when the panel opens/closes programmatically:

'use client';

import { useState } from 'react';
import { HazoNotesIcon } from 'hazo_notes';

export default function ControlledOpenExample() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Notes</button>
      <HazoNotesIcon
        ref_id="controlled-open"
        label="Controlled Open"
        open={isOpen}
        onOpenChange={setIsOpen}
      />
    </>
  );
}

Note: When using controlled open mode, the component applies a small delay (100ms) before opening the panel. This prevents conflicts when opening notes from dropdown menus or other overlays that need to close first.

Configuration

Configuration options in config/hazo_notes_config.ini:

[ui]
# Background color for notes panel (Tailwind CSS class)
background_color = bg-yellow-100

# Panel presentation style: popover | slide_panel
panel_style = popover

# Save behavior: explicit | auto
save_mode = explicit

[storage]
# File storage mode: jsonb | filesystem
file_storage_mode = jsonb

# Path for filesystem storage (only used when file_storage_mode = filesystem)
file_storage_path = /uploads/notes

[files]
# Maximum file size in MB
max_file_size_mb = 10

# Allowed file types (comma-separated extensions, no dots)
allowed_file_types = pdf,png,jpg,jpeg,gif,doc,docx

# Maximum files per single note entry
max_files_per_note = 5

[logging]
# Log file path (relative to application root)
logfile = logs/hazo_notes.log

Component API

HazoNotesIcon Props

interface HazoNotesIconProps {
  // Required
  ref_id: string;                      // Unique identifier for this notes instance

  // Display
  label?: string;                      // Panel header label (default: 'Notes')
  has_notes?: boolean;                 // Override indicator when notes exist
  note_count?: number;                 // Override display count badge

  // Controlled mode (notes state)
  notes?: NoteEntry[];                 // Controlled notes array
  on_notes_change?: (notes: NoteEntry[]) => void;

  // Controlled mode (open state)
  open?: boolean;                      // Control panel open state
  onOpenChange?: (open: boolean) => void;
  default_open?: boolean;              // Initial open state (uncontrolled)

  // User context
  current_user?: NoteUserInfo;         // User info (auto-fetched if not provided)

  // Configuration overrides
  panel_style?: 'popover' | 'slide_panel';
  save_mode?: 'explicit' | 'auto';
  background_color?: string;           // Tailwind class

  // File options
  enable_files?: boolean;              // Enable file attachments (default: true)
  max_files_per_note?: number;         // Default: 5
  allowed_file_types?: string[];       // Default: ['pdf', 'png', 'jpg', ...]
  max_file_size_mb?: number;           // Default: 10

  // Callbacks
  on_open?: () => void;                // Called when panel opens
  on_close?: () => void;               // Called when panel closes

  // Styling
  disabled?: boolean;                  // Disable and hide the component
  className?: string;                  // Additional CSS classes
  icon_size?: number;                  // Button size in pixels (default: 28)
  show_border?: boolean;               // Show border around button (default: true)
}

Note: The component bundles Radix UI primitives internally. No UI component props are required.

Hooks API

use_notes

Manages notes state and API interactions.

import { use_notes } from 'hazo_notes/hooks';

function MyComponent({ refId }: { refId: string }) {
  const {
    notes,
    note_count,
    loading,
    error,
    add_note,
    refresh,
  } = use_notes(refId);

  const handleAddNote = async () => {
    const success = await add_note('This is my note');
    if (success) {
      console.log('Note added!');
    }
  };

  if (loading) return <div>Loading notes...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <p>{note_count} notes</p>
      <button onClick={handleAddNote}>Add Note</button>
    </div>
  );
}

use_notes_file_upload

Handles file uploads and validation.

import { use_notes_file_upload } from 'hazo_notes/hooks';

function FileUploadExample() {
  const {
    pending_files,
    upload_file,
    remove_file,
    uploading,
    error,
  } = use_notes_file_upload({
    ref_id: 'my-notes',
    max_file_size_mb: 5,
  });

  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const uploaded = await upload_file(file, 'attachment');
      if (uploaded) {
        console.log('File uploaded:', uploaded.filename);
      }
    }
  };

  return (
    <div>
      <input type="file" onChange={handleFileSelect} disabled={uploading} />
      {pending_files.map(f => (
        <div key={f.file_no}>
          {f.filename}
          <button onClick={() => remove_file(f.file_no)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

File Attachments

Notes support inline file references in text:

Embed Mode (Images Inline)

Check out this screenshot:
<<embed:0001>>

Result: Image displays directly in the note.

Download the full report:
<<attach:0001>>

Result: Shows as a clickable download link with file icon.

Paste to Embed

Users can paste images directly into the note textarea - they're automatically uploaded and referenced with <<embed:XXXX>> syntax.

File Storage Modes

JSONB Mode (Default)

[storage]
file_storage_mode = jsonb
  • Files stored as Base64 in database
  • Simpler setup (no file API needed)
  • Good for small files (< 1MB)
  • Works out of the box

Filesystem Mode

[storage]
file_storage_mode = filesystem
file_storage_path = /uploads/notes
  • Files stored on server filesystem
  • Better for large files
  • Requires file upload API route

Create app/api/hazo_notes/files/upload/route.ts:

import { createFilesHandler } from 'hazo_notes/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';

export const dynamic = 'force-dynamic';

const { POST } = createFilesHandler({
  getHazoConnect: () => getHazoConnectSingleton(),
  getUserIdFromRequest: async (req) => {
    const session = await getSession(req);
    return session?.user?.id || null;
  },
  file_storage_mode: 'filesystem',
  file_storage_path: '/uploads/notes',
  max_file_size_mb: 10,
  allowed_file_types: ['pdf', 'png', 'jpg', 'jpeg', 'gif'],
});

export { POST };

Logger Integration (Optional)

Client-Side

// app/providers.tsx
'use client';

import { LoggerProvider } from 'hazo_notes';
import { createClientLogger } from 'hazo_logs/ui';

const logger = createClientLogger({ packageName: 'my_app' });

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LoggerProvider logger={logger}>
      {children}
    </LoggerProvider>
  );
}

Server-Side

// lib/logger-setup.ts
import { set_server_logger } from 'hazo_notes/lib';
import { createLogger } from 'hazo_logs';

export function initializeLogger() {
  set_server_logger(createLogger('hazo_notes'));
}

TypeScript Types

All types are exported from hazo_notes/types:

import type {
  NoteEntry,
  NoteFile,
  NoteUserInfo,
  HazoNotesIconProps,
  HazoNotesPanelProps,
} from 'hazo_notes/types';

Database Schema

The hazo_notes table stores all notes:

CREATE TABLE hazo_notes (
  id UUID PRIMARY KEY,
  ref_id UUID NOT NULL,              -- Links to parent entity
  note JSONB NOT NULL DEFAULT '[]',  -- Array of note entries
  created_at TIMESTAMPTZ NOT NULL,
  changed_at TIMESTAMPTZ,
  note_count INTEGER NOT NULL DEFAULT 0
);

Each note entry in the JSONB array:

{
  userid: "user-uuid",
  created_at: "2026-01-07T12:30:00.000Z",
  note_text: "This is the note content",
  note_files: [
    {
      file_no: "0001",
      embed_type: "embed",
      filename: "screenshot.png",
      filedata: "base64_data_or_file_path",
      mime_type: "image/png",
      file_size: 12345
    }
  ]
}

Troubleshooting

Notes icon doesn't render

Problem: The HazoNotesIcon component doesn't appear.

Possible Causes:

  1. Missing ref_id prop - The component requires a valid ref_id and will not render without one
  2. disabled={true} is set
  3. Tailwind CSS is not configured

Solution:

// Check that ref_id is provided and valid
<HazoNotesIcon
  ref_id="my-field-123"  // Required - must be a non-empty string
  label="My Notes"
/>

In development, a console warning will appear if ref_id is missing.

User shows as "Unknown User"

Problem: Notes display but no user names.

Solution: Implement getUserProfile in your API handler:

getUserProfile: async (userId) => {
  const user = await fetchUserFromDatabase(userId);
  return {
    id: userId,
    name: user.name,
    email: user.email,
    profile_image: user.avatar,
  };
}

Notes don't persist

Problem: Notes disappear after refresh.

Solution:

  1. Verify database table exists (run migration)
  2. Check ref_id is consistent
  3. Verify API route is working: curl http://localhost:3000/api/hazo_notes/test-id

File upload fails

Problem: Can't upload files.

Solution:

  • For JSONB mode: Should work out of the box
  • For filesystem mode: Create the files upload API route (see File Storage Modes above)

Authentication errors

Problem: 401 Unauthorized on GET or POST.

Solution: Wire getAuth (preferred, hazo_auth@6.x) or getUserIdFromRequest (hazo_auth@5.x). GET and POST both require a non-null user; both return 401 if the resolver returns null.

// hazo_auth@6.x — preferred
getAuth: async (req) => {
  const auth = await hazo_get_tenant_auth(req as NextRequest);
  if (!auth.authenticated || !auth.user?.id) return null;
  return { user_id: auth.user.id, scope_id: auth.selected_scope_id ?? null };
}

// hazo_auth@5.x — backward-compatible fallback
getUserIdFromRequest: async (req) => {
  const session = await getSession(req);
  return session?.user?.id ?? null;
}

PostgREST permission denied for table hazo_notes (500 with cause 42501)

Problem: GET/POST return 500. Response includes "cause": "permission denied for table hazo_notes" (when expose_error_cause is enabled).

Solution: Apply migrations/002_grant_api_user_hazo_notes.sql. It grants SELECT, INSERT, UPDATE, DELETE on hazo_notes to the api_user role used by PostgREST. Reproducing the underlying error directly:

curl "$POSTGREST_URL/hazo_notes?limit=1"
# Before grant: 401 / "permission denied for table hazo_notes"
# After grant:  [] or rows

Examples

See the test-app/ directory for complete working examples:

  • Basic notes: Simple note creation and display
  • Popover style: Notes in a popover
  • Slide panel style: Notes in a slide-out panel
  • With files: File attachment demonstrations
  • Auto-save: Auto-save mode example
  • Multiple instances: Multiple independent notes on one page
  • Controlled mode: Parent state integration

Run the test app:

npm run dev:test-app
# Open http://localhost:3002

Contributing

See SETUP_CHECKLIST.md for development setup instructions.

License

MIT

Support