Skip to content

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 annotation
await env.ANNOTATION.put(mrnObject, "Configuration data");
// Retrieve an annotation
const data = await env.ANNOTATION.get(mrnObject);
// List annotations with filtering
const 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 storage
  • applicationName: Your application’s name from the manifest
  • versionId: Version identifier for the deployment

Optional Fields:

  • module: Module or service name within your app
  • item: Specific resource identifier
  • key: Metadata key for the item
  • revision: 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 string
  • limit: 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']
}));

Data Retrieval Examples

// Get JSON data
const 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 content
const 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 annotations
const 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 module
const moduleAnnotations = await env.ANNOTATION.list({
prefix: 'annotation:my-app:v1.0.0:payment:'
});
console.log(`Found ${moduleAnnotations.objects.length} annotations`);
// List with pagination
const 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 pagination
if (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 1
const 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 2
const 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 revision
const 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"
}