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_TOKEN
env var) - Email sender allowlist (via
INCOMING_EMAIL_ALLOWLIST
env 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/processed
to 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
Clients that consume this inbox should:
- Poll
GET /
endpoint for unprocessed items (or use webhooks if added) - Get attachment metadata via
GET /:recordId/attachments
and download blobs - Process items into Obsidian vault format
- Mark each item as processed via
PATCH /:id/processed
- Handle failures gracefully (service doesn't retry)
Client type is TBD - may be MCP servers, custom scripts, or other automation.
- All code must be compatible with Deno runtime
- Use
https://esm.sh
for npm imports - Storage limited to SQLite + blob storage (no filesystem)
- See
.cursor/rules/townie.mdc
for full Val.town development guidelines
Schema Changes:
- Modify
inboxRecordSchema
in 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)
├── service.ts # Main service entry point (routes to handlers)
├── InboxService.ts # Inbox record persistence
├── AttachmentService.ts # Blob storage for attachments
├── auth.ts # Authentication/authorization
├── deno.json # Deno/Val.town configuration
└── .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.