Skip to content

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-in
export 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 storage
  • applicationName: Application name from your manifest
  • versionId: Version identifier for deployment

Optional Fields:

  • module: Service or component name within your application
  • item: Specific resource, user, or entity identifier
  • key: Metadata key for the item
  • revision: 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 annotation
const 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 annotations
const 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 configuration
const 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 data
await this.env.ANNOTATION.put({
type: 'annotation',
applicationName: 'my-app',
versionId: 'v1.0.0',
key: 'deployment-notes'
}, 'Deployed with new payment gateway integration');
// Store binary data
const 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 prefix
  • limit: number - Maximum results to return
  • cursor: string - Pagination token from previous call
  • delimiter: string - Group results by common prefixes

Returns: Promise<BucketListResult> - List result with objects array

// List all annotations for a module
const moduleAnnotations = await this.env.ANNOTATION.list({
prefix: 'annotation:ecommerce-app:v2.1.0:payment:'
});
console.log(`Found ${moduleAnnotations.objects.length} annotations`);
// Paginated listing
const 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 pagination
if (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 annotations
const 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 annotations
export 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 data
await 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 configuration
const 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;
}

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:
```hcl
application "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
}
}