Annotations
Overview
The Raindrop annotation interface provides structured metadata storage using Machine-Readable Name (MRN) objects for addressing. Access the interface through env.ANNOTATION
in your services and actors. This system acts as a key-value store specifically designed for application metadata, with built-in hierarchical organization and versioning capabilities.
The annotation system stores metadata with automatic revision management, making it suitable for configuration data, documentation, debugging information, and application metadata that needs hierarchical organization. Unlike traditional databases, annotations are optimized for metadata storage patterns where you need to associate information with specific application components, modules, or individual items. The revision system ensures you can track changes over time without manual version management.
Each annotation is identified by an MRN object that specifies the application, version, optional module, optional item, and optional key. The system handles revision numbering automatically when storing data. This addressing scheme allows you to organize metadata at different levels of granularity, from application-wide settings to specific item properties, while maintaining clear relationships between different pieces of metadata.
Prerequisites
- Basic understanding of Raindrop applications and services
- Knowledge of your application’s module structure for building MRN objects
Getting Started
Annotation storage is automatically available in all Raindrop applications. Access the interface through env.ANNOTATION
:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const metadata = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'user-service', key: 'cache-config' });
return new Response(metadata ? 'Found config' : 'No config'); }}
Basic Usage
The annotation interface provides three core methods:
// Store an annotationawait env.ANNOTATION.put(mrnObject, "Configuration data");
// Retrieve an annotationconst data = await env.ANNOTATION.get(mrnObject);
// List annotations with filteringconst results = await env.ANNOTATION.list({ prefix: "user-service:" });
All operations use MRN objects to identify the specific annotation target.
Core Concepts
MRNObject Interface
All annotation operations use MRNObject
to identify annotation targets. The MRN (Machine-Readable Name) system provides a hierarchical addressing scheme that allows you to organize annotations at different levels of specificity, from application-wide metadata down to individual item properties. The optional fields create a flexible hierarchy that can adapt to your application’s structure:
type MRNObject = { type: 'annotation' | 'label'; applicationName: string; versionId: string; module?: string; item?: string; key?: string; revision?: string;};
Required Fields:
type
: Must be'annotation'
for metadata storageapplicationName
: Your application’s name from the manifestversionId
: Version identifier for the deployment
Optional Fields:
module
: Module or service name within your appitem
: Specific resource identifierkey
: Metadata key for the itemrevision
: Specific revision (managed automatically, omit for latest)
Interface Methods
get()
get(mrn: MRNObject): Promise<BucketObjectBody | null>
Retrieves annotation data for the specified MRN object. This method returns the most recent revision of the annotation, or null if no annotation exists at the specified MRN path. The returned BucketObjectBody provides multiple methods for accessing the stored data depending on its format.
Parameters:
mrn
: MRNObject identifying the annotation
Returns: BucketObjectBody with data access methods, or null
if not found
const config = await env.ANNOTATION.get({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'payment', key: 'stripe-config'});
if (config) { const configText = await config.text(); const configJson = await config.json(); const configBuffer = await config.arrayBuffer();}
put()
put(mrn: MRNObject, data: BucketPutValue, options?: BucketPutOptions): Promise<BucketObjectBody>
Stores annotation data with automatic revision management. Each call to put() creates a new revision, with the system automatically incrementing the revision number. This allows you to update annotation data without losing previous versions, which is particularly useful for configuration changes or audit trails.
Parameters:
mrn
: MRNObject identifying the annotation (must not include revision)data
: Data to store (string, ReadableStream, ArrayBuffer, Blob, or null)options
: Optional BucketPutOptions for metadata and configuration
Returns: BucketObjectBody with the stored object details including assigned revision
const result = await env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'payment', key: 'stripe-config'}, JSON.stringify({ publishableKey: 'pk_test_...', webhookEndpoint: '/webhook/stripe'}));
console.log('Stored with revision:', result.key);
list()
list(options?: BucketListOptions): Promise<BucketListResult>
Lists annotations with optional filtering and pagination. This method is useful for discovering annotations within a specific scope, such as all annotations for a module or all annotations matching a naming pattern. The prefix parameter allows you to filter results based on the MRN structure, making it easy to find related annotations.
Parameters:
options
: Optional BucketListOptions for filtering and pagination
Returns: BucketListResult containing matching annotations
Options:
prefix
: Filter results by MRN prefix stringlimit
: Maximum results to return (number)cursor
: Pagination token from previous call (string)delimiter
: Group results by common prefixes (string)
const moduleAnnotations = await env.ANNOTATION.list({ prefix: 'annotation:ecommerce-app:v2.1.0:payment:'});
const batch = await env.ANNOTATION.list({ prefix: 'annotation:ecommerce-app:v2.1.0:', limit: 50, cursor: previousCursor});
console.log('Found', batch.objects.length, 'annotations');
MRN Object Examples
Application-Level
Target the entire application by omitting module, item, and key:
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', key: 'deployment-notes'}, 'Version 2.1.0 deployment notes');
Module-Level
Target a specific module by providing the module field:
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'user-service', key: 'rate-limits'}, JSON.stringify({ login: 5, signup: 2 }));
Item-Level
Target specific items within a module:
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'inventory', item: 'product-12345', key: 'audit-log'}, 'Product updated by admin');
Multiple Keys per Item
Organize multiple annotations for the same item using different keys:
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'users', item: 'user-789', key: 'preferences'}, JSON.stringify({ theme: 'dark', notifications: true }));
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'users', item: 'user-789', key: 'permissions'}, JSON.stringify({ canDelete: false, canView: true }));
Code Examples
Data Type Examples
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'api-client', key: 'config'}, JSON.stringify({ maxRetries: 3, timeout: 5000, endpoints: ['api.example.com', 'backup.example.com']}));
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v2.1.0', key: 'release-notes'}, 'v2.1.0: Added payment gateway, fixed auth bug');
const binaryData = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
await env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'assets', item: 'logo', key: 'image-data'}, binaryData.buffer);
Data Retrieval Examples
// Get JSON dataconst configResult = await env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'api-client', key: 'config'});
if (configResult) { const config = JSON.parse(await configResult.text());}
// Get text contentconst notesResult = await env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v2.1.0', key: 'release-notes'});
if (notesResult) { const notes = await notesResult.text();}
// Handle missing annotationsconst result = await env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', key: 'nonexistent'});
if (!result) { console.log('Annotation not found');}
List Examples
// List annotations for a specific moduleconst moduleAnnotations = await env.ANNOTATION.list({ prefix: 'annotation:my-app:v1.0.0:payment:'});
console.log(`Found ${moduleAnnotations.objects.length} annotations`);
// List with paginationconst batch = await env.ANNOTATION.list({ prefix: 'annotation:my-app:v1.0.0:', limit: 10});
batch.objects.forEach(obj => { console.log('Key:', obj.key); console.log('Size:', obj.size, 'bytes');});
// Continue paginationif (batch.truncated && batch.cursor) { const nextBatch = await env.ANNOTATION.list({ prefix: 'annotation:my-app:v1.0.0:', limit: 10, cursor: batch.cursor });}
Revision Examples
// First put creates revision 1const firstResult = await env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'settings', key: 'theme'}, 'light');
console.log('Revision:', firstResult.key);// annotation:my-app:v1.0.0:settings^theme:1
// Second put creates revision 2const secondResult = await env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'settings', key: 'theme'}, 'dark');
console.log('Revision:', secondResult.key);// annotation:my-app:v1.0.0:settings^theme:2
// Get returns latest revisionconst current = await env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'settings', key: 'theme'});
if (current) { console.log('Theme:', await current.text()); // "dark"}