A Val.town service that acts as a temporary inbox for raw information before it's processed into an Obsidian vault.
This service provides a buffer layer between various information sources (email, HTTP POST) and Obsidian vault management systems. It:
- Receives raw information from multiple sources
- Stores unprocessed items with attachments
- Provides a simple API for clients to retrieve and process items
- Manages the lifecycle of inbox items (unprocessed → processed → deleted)
- Runtime: Deno on Val.town serverless environment
- Database: SQLite (Val.town standard library)
- Storage: Blob storage (Val.town standard library)
- Language: TypeScript
-
emailHandler.ts: Email trigger that processes incoming emails
- Validates sender against allowlist
- Extracts email content and metadata
- Saves attachments to blob storage
- Creates inbox record
-
httpHandler.ts: HTTP API using Hono framework
GET /- Retrieve all unprocessed itemsPOST /- Create new inbox item (for non-email sources)PATCH /:id/processed- Mark item as processed (returns 404 if record not found)GET /:recordId/attachments- Get attachment metadata for a record
-
InboxService.ts: Core data persistence for inbox records
- Stores records in SQLite with UUID keys
- Tracks processing state (unprocessed/processed timestamp)
- Provides lifecycle operations (save, markProcessed, retrieveUnprocessed)
- Auto-deletes processed records after 30 days
-
AttachmentService.ts: Blob storage for email attachments
- Stores binary data and metadata separately
- Supports get/save/delete operations
- Uses UUID keys for blob identification
-
auth.ts: Authentication and authorization
- Bearer token auth for HTTP endpoints (via
API_TOKENenv var) - Email sender allowlist (via
INCOMING_EMAIL_ALLOWLISTenv var)
- Bearer token auth for HTTP endpoints (via
{
raw: string, // Full text content (email body, etc.)
summary?: string, // Optional summary
from: string, // Source identifier (email address, etc.)
subject: string, // Subject line or title
date: Date, // When item was received
attachmentBlobIds: string[] // References to blob storage
}
Stored in SQLite table inbox_records with:
id(UUID primary key)record(JSON-serialized InboxRecord)created_at(timestamp)processed_at(nullable timestamp)
{
filename: string,
contentType: string,
size: number,
data: ArrayBuffer // The actual binary content
}
Stored in blob storage as:
{uuid}- binary data{uuid}:meta- JSON metadata
- Ingestion: Item arrives via email or HTTP POST
- Storage: Saved to SQLite with
processed_at = null - Retrieval: Client calls
GET /to fetch unprocessed items - Processing: Client transforms item into Obsidian note(s) (outside this service)
- Completion: Client calls
PATCH /:id/processedto mark as done - Cleanup: After 30 days, processed records auto-delete (including attachments)
Note: Clients are responsible for idempotency. If processing fails partway through, the client should handle re-processing gracefully.
Required configuration via Val.town secrets:
API_TOKEN- Bearer token for HTTP API authenticationINCOMING_EMAIL_ALLOWLIST- Comma-separated email addresses allowed to send
A Model Context Protocol (MCP) server is provided that runs locally and communicates with the remote API. This allows Claude Desktop to interact with your inbox.
Setup: See MCP-README.md for complete setup instructions.
Architecture: The MCP server (mcp.ts) runs on your local machine and uses apiClient.ts to call the remote Val.town HTTP API. This keeps the MCP logic separate from the serverless service.
Other clients that consume this inbox should:
- Poll
GET /api/inboxendpoint for unprocessed items - Get attachment metadata via
GET /api/record/:recordId/attachments - Download attachments via
GET /api/record/:recordId/attachments/:blobId - Process items into desired format (e.g., Obsidian notes)
- Mark each item as processed via
PATCH /api/record/:id/processed - Handle failures gracefully (service doesn't retry)
All API calls require Bearer token authentication.
- All code must be compatible with Deno runtime
- Use
https://esm.shfor npm imports - Storage limited to SQLite + blob storage (no filesystem)
- See
.cursor/rules/townie.mdcfor full Val.town development guidelines
Schema Changes:
- Modify
inboxRecordSchemain InboxService.ts - Change table name (e.g.,
inbox_records→inbox_records_2) to avoid migration issues - Update both email and HTTP handlers if needed
Adding Input Sources:
- Create new handler file (e.g.,
webhookHandler.ts) - Map to appropriate trigger type (HTTP, cron, etc.)
- Transform input to InboxRecord format
- Call
InboxService.save()
Processing Logic Changes:
- Most business logic lives in handlers (emailHandler.ts, httpHandler.ts)
- Keep services (InboxService, AttachmentService) focused on data operations
- Prefer small, focused functions over complex class hierarchies
- Manual testing via Val.town web interface
- No formal test suite currently
- Focus on edge cases: malformed emails, missing attachments, auth failures
- Error handling and retry logic for failed processing
- Consider webhook/push notification when new items arrive (vs polling)
- Finalize client architecture (MCP vs custom scripts)
- Evaluate if 30-day retention is appropriate for all use cases
.
├── emailHandler.ts # Email trigger entry point
├── httpHandler.ts # HTTP API entry point (Hono app)
├── InboxService.ts # Inbox record persistence
├── AttachmentService.ts # Blob storage for attachments
├── auth.ts # Authentication/authorization
├── db.ts # Database schema and connection
├── types.ts # Shared TypeScript types
├── mcp.ts # MCP server (runs locally)
├── apiClient.ts # HTTP client for remote API
├── deno.json # Deno/Val.town configuration
├── MCP-README.md # MCP server setup guide
├── mcp-config.example.json # Example Claude Desktop config
└── .cursor/rules/
└── townie.mdc # Val.town development guidelines
- Simple buffer: This is NOT a full email client or note-taking app. It's a temporary holding area.
- Client-side processing: Complex transformations happen in clients, not here.
- Stateless: Each inbox item is independent. No complex workflows or state machines.
- Val.town native: Leverage platform services (SQLite, blob, email) rather than external dependencies.
- Fail-fast: Let errors bubble up with context rather than hiding them.