Database-backed notes system with file attachment support for the hazo ecosystem
npm install hazo_notesDatabase-backed notes system with file attachment support for Next.js applications in the hazo ecosystem.
- 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 < and <
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
``bash`
npm install hazo_notes
Install these based on your needs:
`bash`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.
`tsx
import { HazoNotesIcon } from 'hazo_notes';
function MyComponent() {
return (
The component bundles its own Radix UI primitives, so no additional UI component setup is required.
$3
Create
app/api/hazo_notes/[ref_id]/route.ts:`typescript
import { createNotesHandler } from 'hazo_notes/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
// Import your auth and user lookup functions
import { getSession } from '@/lib/auth'; // Replace with your auth
import { getUserById } from '@/lib/users'; // Replace with your user lookupexport const dynamic = 'force-dynamic';
const { GET, POST } = createNotesHandler({
getHazoConnect: () => getHazoConnectSingleton(),
getUserIdFromRequest: async (req) => {
// IMPORTANT: Replace with your authentication logic
const session = await getSession(req);
return session?.user?.id || null;
},
getUserProfile: async (userId) => {
// IMPORTANT: Replace with your user profile lookup
const user = await getUserById(userId);
return {
id: userId,
name: user?.name || 'Unknown User',
email: user?.email || '',
profile_image: user?.avatar,
};
},
});
export { GET, POST };
`$3
Run the migration:
PostgreSQL:
`sql
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);
`SQLite:
`sql
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);
`$3
Copy the config template:
`bash
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
$3
`tsx
import { HazoNotesIcon } from 'hazo_notes';export default function FormPage() {
return (
ref_id="income-field"
label="Annual Income"
/>
);
}
`Result: Click the notes icon to add contextual notes about this field.
$3
`tsx
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
< or <
- Images display inline, other files show as download links$3
`tsx
ref_id="detailed-notes"
label="Detailed Notes"
panel_style="slide_panel"
/>
`Result: Notes open in a slide-out panel instead of a popover.
$3
`tsx
ref_id="quick-notes"
label="Quick Notes"
save_mode="auto"
/>
`Result: Notes save automatically when panel closes (no save/cancel buttons).
$3
`tsx
ref_id="styled-notes"
label="Styled Notes"
background_color="bg-blue-50"
icon_size={24}
show_border={false}
className="ml-2"
/>
`$3
`tsx
'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([]);
return (
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.
$3
Control when the panel opens/closes programmatically:
`tsx
'use client';import { useState } from 'react';
import { HazoNotesIcon } from 'hazo_notes';
export default function ControlledOpenExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
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:`ini
[ui]
Background color for notes panel (Tailwind CSS class)
background_color = bg-yellow-100Panel presentation style: popover | slide_panel
panel_style = popoverSave behavior: explicit | auto
save_mode = explicit[storage]
File storage mode: jsonb | filesystem
file_storage_mode = jsonbPath 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 = 10Allowed file types (comma-separated extensions, no dots)
allowed_file_types = pdf,png,jpg,jpeg,gif,doc,docxMaximum 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
$3
`typescript
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
$3
Manages notes state and API interactions.
`typescript
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
Loading notes...;
if (error) return Error: {error}; return (
{note_count} notes
);
}
`$3
Handles file uploads and validation.
`typescript
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) => {
const file = e.target.files?.[0];
if (file) {
const uploaded = await upload_file(file, 'attachment');
if (uploaded) {
console.log('File uploaded:', uploaded.filename);
}
}
};
return (
{pending_files.map(f => (
{f.filename}
))}
);
}
`File Attachments
Notes support inline file references in text:
$3
`
Check out this screenshot:
<>
`Result: Image displays directly in the note.
$3
`
Download the full report:
<>
`Result: Shows as a clickable download link with file icon.
$3
Users can paste images directly into the note textarea - they're automatically uploaded and referenced with
< syntax.File Storage Modes
$3
`ini
[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
$3
`ini
[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:`typescript
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)
$3
`tsx
// 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 (
{children}
);
}
`$3
`typescript
// 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:`typescript
import type {
NoteEntry,
NoteFile,
NoteUserInfo,
HazoNotesIconProps,
HazoNotesPanelProps,
} from 'hazo_notes/types';
`Database Schema
The
hazo_notes table stores all notes:`sql
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:
`typescript
{
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
$3
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 configuredSolution:
`tsx
// Check that ref_id is provided and valid
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.$3
Problem: Notes display but no user names.
Solution: Implement
getUserProfile in your API handler:
`typescript
getUserProfile: async (userId) => {
const user = await fetchUserFromDatabase(userId);
return {
id: userId,
name: user.name,
email: user.email,
profile_image: user.avatar,
};
}
`$3
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$3
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)
$3
Problem: "Unauthorized" when adding notes.
Solution: Implement
getUserIdFromRequest to return authenticated user ID:
`typescript
getUserIdFromRequest: async (req) => {
const session = await getSession(req);
if (!session?.user?.id) return null;
return session.user.id;
}
`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:
`bash
npm run dev:test-app
Open http://localhost:3002
``See SETUP_CHECKLIST.md for development setup instructions.
MIT
- hazo_connect - Database connection abstraction
- hazo_auth - Authentication and user management
- hazo_logs - Structured logging
- hazo_config - INI configuration management
- Issues: https://github.com/pub12/hazo_notes/issues
- Documentation: https://github.com/pub12/hazo_notes#readme