Annotations
This content is for the 0.6.2 version. Switch to the latest version for up-to-date documentation.
Annotations
Overview
The Raindrop annotation interface provides structured metadata storage using Machine-Readable Name (MRN) objects for addressing. This system acts as a key-value store designed for application metadata with hierarchical organization and automatic versioning.
Annotations store metadata with automatic revision management, making them suitable for configuration data, documentation, debugging information, and application metadata. Unlike traditional databases, annotations optimize for metadata storage patterns where you associate information with specific application components, modules, or individual items.
Key benefits include hierarchical MRN addressing, automatic revision tracking, flexible data format support, and direct access from all services and actors.
Prerequisites
- Active Raindrop application with services or actors
- Understanding of your application’s module and component structure
- Familiarity with hierarchical naming schemes and metadata organization
- Knowledge of JSON data structures for configuration storage
Creating/Getting Started
Annotations are automatically available in all Raindrop applications without additional configuration. Access through the env.ANNOTATION
interface:
// No manifest configuration required - annotations are built-inexport default class extends Service<Env> { async fetch(request: Request): Promise<Response> { // Access annotation interface directly const result = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', key: 'app-config' });
return new Response(result ? 'Config found' : 'No config'); }}
Store your first annotation:
await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', key: 'startup-config'}, JSON.stringify({ debugMode: true, maxConnections: 100}));
Accessing/Basic Usage
Use the three core annotation methods for all metadata operations:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname === '/config/get') { // Retrieve annotation const config = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'api', key: 'settings' });
if (config) { const data = await config.json(); return Response.json(data); }
return new Response('Config not found', { status: 404 }); }
if (url.pathname === '/config/set') { // Store annotation const newConfig = await request.json(); await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'api', key: 'settings' }, JSON.stringify(newConfig));
return new Response('Config saved'); }
if (url.pathname === '/config/list') { // List annotations const list = await this.env.ANNOTATION.list({ prefix: 'annotation:my-app:v1.0.0:api:' });
return Response.json({ count: list.objects.length, annotations: list.objects.map(obj => obj.key) }); }
return new Response('Not found', { status: 404 }); }}
Core Concepts
Annotations use Machine-Readable Name (MRN) objects to provide hierarchical addressing that scales from application-wide settings down to individual item metadata.
MRN Hierarchy: The optional fields create a flexible hierarchy - applicationName/versionId (required), then optional module, item, and key. This allows organization at different specificity levels.
Automatic Revisions: Each put()
operation creates a new revision automatically. You don’t specify revision numbers - the system manages them and get()
always returns the latest.
Data Flexibility: Store strings, JSON objects, binary data, or streams. The BucketObjectBody interface provides multiple access methods (text(), json(), arrayBuffer(), etc.).
Prefix Filtering: List operations support prefix-based filtering using the MRN structure, making it easy to find related annotations.
MRNObject Interface
All operations use the MRNObject
type for addressing:
interface MRNObject { type: 'annotation' | 'label'; applicationName: string; versionId: string; module?: string; item?: string; key?: string; revision?: string;}
Required Fields:
type
: Always'annotation'
for metadata storageapplicationName
: Application name from your manifestversionId
: Version identifier for deployment
Optional Fields:
module
: Service or component name within your applicationitem
: Specific resource, user, or entity identifierkey
: Metadata key for the itemrevision
: Specific revision (auto-managed, omit for latest)
Retrieving Annotations
Use get()
to retrieve the latest revision of any annotation by its MRN path.
get(mrn)
get(mrn: MRNObject): Promise<BucketObjectBody | null>
Parameters:
mrn
:MRNObject
- MRN object identifying the target annotation
Returns: BucketObjectBody | null
- Data access object or null if not found
// Get configuration annotationconst config = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'ecommerce-app', versionId: 'v2.1.0', module: 'payment', key: 'stripe-config'});
if (config) { // Access data in different formats const configText = await config.text(); const configJson = await config.json(); const configBuffer = await config.arrayBuffer(); const configBlob = await config.blob();}
// Handle missing annotationsconst result = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', key: 'nonexistent'});
if (!result) { // Use default configuration const defaultConfig = { timeout: 5000 };}
Storing Annotations
Use put()
to store annotations with automatic revision management and flexible data format support.
put(mrn, data, options?)
put(mrn: MRNObject, data: BucketPutValue, options?: BucketPutOptions): Promise<BucketObjectBody>
Parameters:
mrn
:MRNObject
- MRN object identifying storage location (excluding revision)data
:BucketPutValue
- Data to store (string, ReadableStream, ArrayBuffer, Blob, null)options
:BucketPutOptions
- Optional metadata and configuration
Returns: Promise<BucketObjectBody>
- Stored object details including assigned revision
// Store JSON configurationconst result = await this.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', currency: 'usd'}));
console.log('Stored with revision:', result.key);
// Store text dataawait this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', key: 'deployment-notes'}, 'Deployed with new payment gateway integration');
// Store binary dataconst logoData = new Uint8Array([137, 80, 78, 71]);await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'my-app', versionId: 'v1.0.0', module: 'assets', item: 'logo', key: 'image-data'}, logoData.buffer);
Listing Annotations
Use list()
to discover annotations with prefix filtering and pagination support.
list(options?)
list(options?: BucketListOptions): Promise<BucketListResult>
Parameters:
options
:BucketListOptions
- Optional filtering and pagination
Options:
prefix
:string
- Filter by MRN prefixlimit
:number
- Maximum results to returncursor
:string
- Pagination token from previous calldelimiter
:string
- Group results by common prefixes
Returns: Promise<BucketListResult>
- List result with objects array
// List all annotations for a moduleconst moduleAnnotations = await this.env.ANNOTATION.list({ prefix: 'annotation:ecommerce-app:v2.1.0:payment:'});
console.log(`Found ${moduleAnnotations.objects.length} annotations`);
// Paginated listingconst batch = await this.env.ANNOTATION.list({ prefix: 'annotation:ecommerce-app:v2.1.0:', limit: 10});
batch.objects.forEach(obj => { console.log('Key:', obj.key); console.log('Size:', obj.size, 'bytes'); console.log('Modified:', obj.uploaded);});
// Continue paginationif (batch.truncated && batch.cursor) { const nextBatch = await this.env.ANNOTATION.list({ prefix: 'annotation:ecommerce-app:v2.1.0:', limit: 10, cursor: batch.cursor });}
// List application-level annotationsconst appAnnotations = await this.env.ANNOTATION.list({ prefix: 'annotation:my-app:v1.0.0:'});
Code Examples
Complete implementations demonstrating annotation patterns and common use cases.
Configuration Management Service
// Service that manages application configuration through annotationsexport default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const [, action, module, key] = url.pathname.split('/');
// GET /config/api/settings - retrieve configuration if (request.method === 'GET') { return this.getConfiguration(module, key); }
// POST /config/api/settings - store configuration if (request.method === 'POST') { const data = await request.json(); return this.setConfiguration(module, key, data); }
// GET /config - list all configurations if (url.pathname === '/config') { return this.listConfigurations(); }
return new Response('Not found', { status: 404 }); }
private async getConfiguration(module: string, key: string): Promise<Response> { const config = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'ecommerce-platform', versionId: 'v1.0.0', module, key });
if (!config) { return new Response('Configuration not found', { status: 404 }); }
try { const data = await config.json(); return Response.json(data); } catch { // Return as text if not JSON const text = await config.text(); return new Response(text, { headers: { 'Content-Type': 'text/plain' } }); } }
private async setConfiguration(module: string, key: string, data: any): Promise<Response> { try { await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'ecommerce-platform', versionId: 'v1.0.0', module, key }, JSON.stringify(data));
return Response.json({ success: true, message: `Configuration ${module}.${key} updated` }); } catch (error) { return Response.json({ success: false, error: 'Failed to store configuration' }, { status: 500 }); } }
private async listConfigurations(): Promise<Response> { const results = await this.env.ANNOTATION.list({ prefix: 'annotation:ecommerce-platform:v1.0.0:' });
const configs = results.objects.map(obj => ({ key: obj.key, size: obj.size, lastModified: obj.uploaded, module: this.extractModule(obj.key), configKey: this.extractConfigKey(obj.key) }));
return Response.json({ count: configs.length, configurations: configs }); }
private extractModule(key: string): string { const parts = key.split(':'); return parts.length > 3 ? parts[3] : ''; }
private extractConfigKey(key: string): string { const parts = key.split(':'); return parts.length > 4 ? parts[4].split('^')[0] : ''; }}
User Preferences with MRN Hierarchy
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const userId = this.extractUserId(request);
if (url.pathname.startsWith('/preferences/')) { const action = url.pathname.split('/')[2];
if (action === 'get') { return this.getUserPreferences(userId); }
if (action === 'set' && request.method === 'POST') { const preferences = await request.json(); return this.setUserPreferences(userId, preferences); }
if (action === 'theme' && request.method === 'POST') { const { theme } = await request.json(); return this.setUserTheme(userId, theme); } }
return new Response('Not found', { status: 404 }); }
private async getUserPreferences(userId: string): Promise<Response> { // Get all user preference annotations const preferences = await this.env.ANNOTATION.list({ prefix: `annotation:user-app:v1.0.0:users:${userId}:` });
const userPrefs: Record<string, any> = {};
// Retrieve each preference value for (const obj of preferences.objects) { const keyParts = obj.key.split(':'); const prefKey = keyParts[keyParts.length - 1].split('^')[0];
const value = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'user-app', versionId: 'v1.0.0', module: 'users', item: userId, key: prefKey });
if (value) { try { userPrefs[prefKey] = JSON.parse(await value.text()); } catch { userPrefs[prefKey] = await value.text(); } } }
return Response.json({ userId, preferences: userPrefs }); }
private async setUserPreferences(userId: string, preferences: Record<string, any>): Promise<Response> { const updates = [];
for (const [key, value] of Object.entries(preferences)) { const updatePromise = this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'user-app', versionId: 'v1.0.0', module: 'users', item: userId, key }, JSON.stringify(value));
updates.push(updatePromise); }
await Promise.all(updates);
return Response.json({ success: true, message: `Updated ${updates.length} preferences for user ${userId}` }); }
private async setUserTheme(userId: string, theme: string): Promise<Response> { await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'user-app', versionId: 'v1.0.0', module: 'users', item: userId, key: 'theme' }, theme);
return Response.json({ success: true, userId, theme }); }
private extractUserId(request: Request): string { // Extract from auth header, session, etc. const authHeader = request.headers.get('Authorization'); return authHeader?.replace('Bearer ', '') || 'anonymous'; }}
Application Configuration with Different Data Types
// Store structured configuration dataawait this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'api-service', versionId: 'v2.1.0', module: 'payment', key: 'gateway-config'}, JSON.stringify({ stripe: { publishableKey: 'pk_test_...', webhookSecret: 'whsec_...', currency: 'usd' }, paypal: { clientId: 'paypal_client_...', environment: 'sandbox' }, defaultGateway: 'stripe'}));
// Retrieve and use configurationconst config = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'api-service', versionId: 'v2.1.0', module: 'payment', key: 'gateway-config'});
if (config) { const paymentConfig = await config.json(); const stripeKey = paymentConfig.stripe.publishableKey;}
// Store deployment notes and documentationawait this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'api-service', versionId: 'v2.1.0', key: 'deployment-notes'}, `Version 2.1.0 Deployment Notes:- Added PayPal payment gateway- Updated Stripe webhook handling- Fixed currency conversion bug- Database migration completed: 2024-01-15`);
// Store API documentationawait this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'api-service', versionId: 'v2.1.0', module: 'users', key: 'api-docs'}, `User API Endpoints:POST /users - Create user accountGET /users/{id} - Retrieve user profilePUT /users/{id} - Update user informationDELETE /users/{id} - Delete user account`);
// Store small binary assets or configuration filesconst iconData = new Uint8Array([ 137, 80, 78, 71, 13, 10, 26, 10, // PNG header 0, 0, 0, 13, 73, 72, 68, 82 // Additional PNG data]);
await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'frontend-app', versionId: 'v1.0.0', module: 'assets', item: 'favicon', key: 'icon-16x16'}, iconData.buffer);
// Retrieve binary dataconst icon = await this.env.ANNOTATION.get({ type: 'annotation', applicationName: 'frontend-app', versionId: 'v1.0.0', module: 'assets', item: 'favicon', key: 'icon-16x16'});
if (icon) { const iconBuffer = await icon.arrayBuffer(); const iconBlob = await icon.blob();}
Revision Tracking Example
export default class extends Service<Env> { async trackConfigurationChange(module: string, key: string, newValue: any, userId: string): Promise<void> { // Store the new configuration const result = await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'config-service', versionId: 'v1.0.0', module, key }, JSON.stringify(newValue));
console.log(`Configuration ${module}.${key} updated to revision ${result.key}`);
// Store audit trail await this.env.ANNOTATION.put({ type: 'annotation', applicationName: 'config-service', versionId: 'v1.0.0', module: 'audit', item: `${module}-${key}`, key: `change-${Date.now()}` }, JSON.stringify({ timestamp: new Date().toISOString(), userId, module, configKey: key, revisionKey: result.key, action: 'update' })); }}
## raindrop.manifest
Annotations are automatically available in all Raindrop applications without explicit configuration:
```hclapplication "config-app" { # Annotations are built-in - no configuration needed
service "config-api" { domain = "config.example.com" # Service can access annotations via env.ANNOTATION }
actor "config-manager" { # Actors can also access annotations via env.ANNOTATION }}