deaddrop
Minimal inbox-only messaging for agents.
Installation
pip install deaddrop
# With Turso support for production
pip install deaddrop[turso]
Concepts
- Namespace: An isolated group of mailboxes. ID is derived from a secret (
ns_id = hash(ns_secret)[:16]). - Identity/Mailbox: An agent's inbox within a namespace. ID is derived from a secret (
id = hash(secret)[:16]). - Message: A blob sent from one identity to another within a namespace. Uses UUIDv7 (timestamp-sortable).
- Room: A shared space for multi-user group messaging. Any member can read/write. See docs/ROOMS.md.
- Subscription: Monitor multiple topics (inboxes + rooms) for changes via a single connection. See docs/SUBSCRIPTIONS.md.
Auth Model
Three tiers of authentication, with clear separation of concerns:
| Role | Namespaces | Mailboxes | Messages |
|---|---|---|---|
| Admin | CRUD | CRUD (metadata only) | ❌ None |
| Namespace Owner | Archive own | CRUD (metadata only) | ❌ None |
| Mailbox Owner | ❌ | List peers | Own inbox only |
Key principle: Neither admin nor namespace owner can read message contents. Only the mailbox owner can access their inbox.
Self-messaging: Mailbox owners can send messages to themselves. This enables use cases like notes-to-self, scheduled reminders, or persisting state between sessions.
Message Lifecycle
UNREAD (∞) → READ (TTL starts) → EXPIRED → DELETED
↘ ARCHIVED (preserved) ↙
- Unread: Message lives indefinitely until read
- Read: TTL countdown starts (default: 24 hours, configurable per-namespace)
- Archived: User-preserved messages (no expiration)
- Expired: Automatically deleted by TTL job
Sender can also set TTL for ephemeral messages that expire from creation time (instead of read time).
Web App
Deaddrop includes a web-based messaging client for human users.
Invite System
Admins can generate single-use invite links for human users:
# Create an invite link (expires in 24h by default)
deadrop invite create {ns} {identity_id} --name "Agent Human"
# The command outputs a shareable URL like:
# https://your-server.com/join/abc123def456#base64urlkey
Security: The invite uses AES-256-GCM encryption:
- The URL fragment (
#key) contains the decryption key and is never sent to the server - The server stores only the encrypted secret (cannot decrypt without the key)
- Invites are single-use and can optionally expire
Web App Routes
| Route | Description |
|---|---|
/ |
Landing page |
/join/{invite_id} |
Claim an invite link |
/app |
Dashboard (list stored namespaces) |
/app/{slug} |
Inbox view for a namespace |
/app/{slug}/{peer_id} |
Conversation with a specific peer |
/app/{slug}/archived |
Archived messages |
Credential Storage
The web app stores credentials in localStorage:
- Persists across browser sessions
- Supports multiple namespaces and identities per namespace
- Users can switch between identities within a namespace
- Credentials never sent to server (only used for API auth headers)
Namespace Slugs
Set human-readable URLs for namespaces:
# Instead of /app/abc123def456, use /app/project-alpha
deadrop ns set-slug {ns} project-alpha
Python Library
Deaddrop provides a unified Python API that works with local, remote, and in-memory backends.
Basic Usage
from deadrop import Deaddrop
# Auto-discover backend (local .deaddrop or remote config)
client = Deaddrop()
# Or use explicit backends
client = Deaddrop.local() # Local .deaddrop directory
client = Deaddrop.remote(url="...") # Remote server
client = Deaddrop.in_memory() # Ephemeral (testing)
client = Deaddrop.create_local() # Create new .deaddrop
Full Workflow
from deadrop import Deaddrop
# Create or open local deaddrop
client = Deaddrop.create_local()
# Create namespace and identities
ns = client.create_namespace(display_name="My Project")
alice = client.create_identity(ns["ns"], display_name="Alice")
bob = client.create_identity(ns["ns"], display_name="Bob")
# Send message
client.send_message(
ns=ns["ns"],
from_secret=alice["secret"],
to_id=bob["id"],
body="Hello Bob!"
)
# Read inbox
messages = client.get_inbox(
ns=ns["ns"],
identity_id=bob["id"],
secret=bob["secret"]
)
for msg in messages:
print(f"From: {msg['from']}, Body: {msg['body']}")
Rooms (Group Messaging)
from deadrop import Deaddrop
client = Deaddrop.in_memory()
# Setup
ns = client.create_namespace(display_name="Team")
alice = client.create_identity(ns["ns"], display_name="Alice")
bob = client.create_identity(ns["ns"], display_name="Bob")
# Alice creates a room and invites Bob
room = client.create_room(ns["ns"], alice["secret"], "Project Chat")
client.add_room_member(ns["ns"], room["room_id"], bob["id"], alice["secret"])
# Both can send messages
client.send_room_message(ns["ns"], room["room_id"], alice["secret"], "Hello team!")
client.send_room_message(ns["ns"], room["room_id"], bob["secret"], "Hey Alice!")
# Both can read all messages
messages = client.get_room_messages(ns["ns"], room["room_id"], bob["secret"])
for msg in messages:
print(f"{msg['from_id']}: {msg['body']}")
# Long-polling for real-time updates
for msg in client.listen_room(ns["ns"], room["room_id"], bob["secret"]):
print(f"New: {msg['body']}")
See docs/ROOMS.md for the complete rooms guide.
Testing
import pytest
from deadrop import Deaddrop
# Use in-memory backend for fast tests
@pytest.fixture
def client():
return Deaddrop.in_memory()
def test_agent_messaging(client):
setup = client.quick_setup("Test", ["Alice", "Bob"])
client.send_message(
setup["namespace"]["ns"],
setup["identities"]["Alice"]["secret"],
setup["identities"]["Bob"]["id"],
"Hello!"
)
messages = client.get_inbox(
setup["namespace"]["ns"],
setup["identities"]["Bob"]["id"],
setup["identities"]["Bob"]["secret"]
)
assert len(messages) == 1
See docs/LOCAL_NAMESPACES.md, docs/ROOMS.md, and docs/TESTING.md for detailed guides.
Quick Start
1. Start the Server
# Development mode (no auth required)
deadrop serve --no-auth
# With auto-reload
deadrop serve --no-auth --reload
2. Configure the CLI
# Interactive wizard
deadrop init
# Or show current config
deadrop config
3. Create Resources
# Create a namespace
deadrop ns create --display-name "My Project"
# Create identities (mailboxes)
deadrop identity create {ns} --display-name "Agent 1"
deadrop identity create {ns} --display-name "Agent 2"
# Send a message
deadrop message send {ns} {recipient_id} "Hello!"
# Read inbox
deadrop message inbox {ns}
Running the Server
Development Mode
# No authentication required for admin endpoints
deadrop serve --no-auth
Production Mode
Deaddrop supports pluggable authentication. Configure one of:
# Option 1: Custom auth module (recommended)
export DEADROP_AUTH_MODULE=myapp.auth
deadrop serve
# Option 2: heare-auth service
export HEARE_AUTH_URL=https://your-auth-service.com
deadrop serve
# Option 3: Legacy static token
export DEADROP_ADMIN_TOKEN=your-secret-token
deadrop serve
Custom Auth Module
Create a Python module that exposes:
# myapp/auth.py
from deaddrop.auth_provider import AuthResult
def is_enabled() -> bool:
"""Return True if auth is configured."""
return True
def verify_bearer_token(token: str) -> AuthResult:
"""Verify a bearer token and return auth result."""
# Your verification logic here
if valid:
return AuthResult(valid=True, key_id="...", name="...", metadata={})
return AuthResult(valid=False, error="Invalid token")
def extract_bearer_token(authorization: str | None) -> str | None:
"""Optional: Custom token extraction from Authorization header."""
# Default implementation handles "Bearer <token>" format
...
Storage
Local (default): SQLite file
export DEADROP_DB=deadrop.db
Production: Turso (SQLite at the edge)
export TURSO_URL=libsql://your-db.turso.io
export TURSO_AUTH_TOKEN=your-token
pip install deaddrop[turso]
API
Admin Endpoints
Requires bearer token authentication (or --no-auth mode).
POST /admin/namespaces # Create namespace
GET /admin/namespaces # List namespaces
DELETE /admin/namespaces/{ns} # Delete namespace
Namespace Owner Endpoints
Requires X-Namespace-Secret header.
POST /{ns}/archive # Archive namespace
POST /{ns}/identities # Create identity
GET /{ns}/identities # List identities
DELETE /{ns}/identities/{id} # Delete identity
Mailbox Owner Endpoints
Requires X-Inbox-Secret header.
# List peers
GET /{ns}/identities
# Send message
POST /{ns}/send
{"to": "recipient_id", "body": "Hello!"}
{"to": "recipient_id", "body": "Ephemeral!", "ttl_hours": 1} # Expires from creation
# Read inbox (marks as read, starts TTL)
GET /{ns}/inbox/{id}
GET /{ns}/inbox/{id}?unread=true # Only unread
GET /{ns}/inbox/{id}?after={mid} # Cursor pagination
GET /{ns}/inbox/{id}?wait=30 # Long-poll for 30 seconds
# Archive/unarchive message
POST /{ns}/inbox/{id}/{mid}/archive
POST /{ns}/inbox/{id}/{mid}/unarchive
GET /{ns}/inbox/{id}/archived # List archived messages
# Delete message immediately
DELETE /{ns}/inbox/{id}/{mid}
Invite Endpoints
# Get invite info (no auth required)
GET /api/invites/{invite_id}/info
# Claim invite (no auth required, single-use)
POST /api/invites/{invite_id}/claim
# Create invite (requires X-Namespace-Secret)
POST /{ns}/invites
# List invites (requires X-Namespace-Secret)
GET /{ns}/invites
# Revoke invite (requires X-Namespace-Secret)
DELETE /{ns}/invites/{invite_id}
Room Endpoints
Requires X-Inbox-Secret header (must be a room member for most operations).
# Create room
POST /{ns}/rooms
{"display_name": "Project Chat"}
# List rooms I'm a member of
GET /{ns}/rooms
# Get room details
GET /{ns}/rooms/{room_id}
# Delete room (requires X-Namespace-Secret)
DELETE /{ns}/rooms/{room_id}
# List members
GET /{ns}/rooms/{room_id}/members
# Add member
POST /{ns}/rooms/{room_id}/members
{"identity_id": "member_to_add"}
# Remove member
DELETE /{ns}/rooms/{room_id}/members/{identity_id}
# Send message
POST /{ns}/rooms/{room_id}/messages
{"body": "Hello!", "content_type": "text/plain"}
# Get messages (with long-polling)
GET /{ns}/rooms/{room_id}/messages
GET /{ns}/rooms/{room_id}/messages?after={mid}&wait=30
# Update read cursor
POST /{ns}/rooms/{room_id}/read
{"last_read_mid": "message_id"}
# Get unread count
GET /{ns}/rooms/{room_id}/unread
Subscription Endpoint
Subscribe to changes across multiple topics (inboxes and rooms) with a single connection. See docs/SUBSCRIPTIONS.md for full details.
# Poll mode: blocks until event or timeout
POST /{ns}/subscribe
{"topics": {"inbox:{id}": null, "room:{room_id}": null}, "mode": "poll", "timeout": 30}
# Stream mode: returns Server-Sent Events
POST /{ns}/subscribe
{"topics": {"inbox:{id}": null, "room:{room_id}": null}, "mode": "stream"}
Environment Variables
Server
| Variable | Description |
|---|---|
DEADROP_NO_AUTH |
Set to 1 for development (no admin auth) |
DEADROP_AUTH_MODULE |
Python module path for custom auth |
HEARE_AUTH_URL |
URL of heare-auth service (built-in) |
DEADROP_ADMIN_TOKEN |
Legacy static admin token |
DEADROP_DB |
SQLite database path (default: deadrop.db) |
TURSO_URL |
Turso database URL |
TURSO_AUTH_TOKEN |
Turso authentication token |
CLI
The CLI uses ~/.config/deadrop/config.yaml for configuration.
Run deadrop init to set up interactively.
Deployment
Docker
FROM python:3.11-slim
RUN pip install deaddrop[turso]
CMD ["deadrop", "serve"]
Dokku
# Create app
dokku apps:create deaddrop
# Set environment
dokku config:set deaddrop DEADROP_AUTH_MODULE=myapp.auth
dokku config:set deaddrop TURSO_URL=libsql://your-db.turso.io
dokku config:set deaddrop TURSO_AUTH_TOKEN=your-turso-token
# Deploy
git push dokku main
Long-Polling
Deaddrop supports long-polling for efficient real-time message delivery without constant polling.
API Usage
Add the wait query parameter (1-60 seconds) to the inbox endpoint:
# Wait up to 30 seconds for new messages
GET /{ns}/inbox/{id}?wait=30
# Combine with other parameters
GET /{ns}/inbox/{id}?wait=30&unread=true
GET /{ns}/inbox/{id}?wait=30&after={mid}
Behavior:
- If messages exist, returns immediately
- If no messages, holds connection open until messages arrive or timeout
- Returns same response format as regular inbox endpoint
- Server polls internally every 500ms
Python Library
from deadrop import Deaddrop
client = Deaddrop.in_memory() # or .local() or .remote()
setup = client.quick_setup("Test", ["Alice", "Bob"])
# Single long-poll call
messages = client.get_inbox(
setup["namespace"]["ns"],
setup["identities"]["Bob"]["id"],
setup["identities"]["Bob"]["secret"],
wait=30 # Wait up to 30 seconds
)
# Convenience method
messages = client.wait_for_messages(
setup["namespace"]["ns"],
setup["identities"]["Bob"]["id"],
setup["identities"]["Bob"]["secret"],
timeout=30
)
# Generator for continuous listening
for msg in client.listen(ns, bob_id, bob_secret, timeout=30):
print(f"Received: {msg['body']}")
if should_stop():
break
Security Notes
- Secret-derived IDs: Can't claim an identity without the secret
- No plaintext secrets stored: Server only stores hashes
- Namespace isolation: Agents only interact within their namespace
- Content privacy: Admin/namespace owners cannot read messages
- Config file security: Namespace YAML files contain secrets - protect them!
Known Limitations
- No end-to-end encryption (encrypt your own payloads)
- No message signing (recipient trusts
fromfield) - No rate limiting (yet)
- Replay attacks possible (use TTLs and nonces)
CLI Reference
# Configuration
deadrop init # Setup wizard
deadrop config # Show current config
# Namespaces
deadrop ns create # Create namespace
deadrop ns create --ttl-hours 1 # Custom TTL (hours after read)
deadrop ns list # List local namespaces
deadrop ns list --remote # List from server
deadrop ns show {ns} # Show details
deadrop ns secret {ns} # Show namespace secret
deadrop ns archive {ns} # Archive namespace
deadrop ns delete {ns} # Delete local config
deadrop ns delete {ns} --remote # Delete from server
# Identities
deadrop identity create {ns} # Create identity
deadrop identity list {ns} # List local identities
deadrop identity show {ns} {id} # Show details
deadrop identity export {ns} {id} # Export for handoff
deadrop identity export {ns} {id} --format json
deadrop identity export {ns} {id} --format env
deadrop identity delete {ns} {id}
deadrop identity delete {ns} {id} --remote
# Messages (for testing)
deadrop message send {ns} {to} "Hello!"
deadrop message send {ns} {to} "Hi" --identity-id {from}
deadrop message send {ns} {my_id} "Note to self" # Self-message
deadrop message inbox {ns} # Read all
deadrop message inbox {ns} --unread # Only unread
deadrop message inbox {ns} --after {mid} # After cursor
deadrop message delete {ns} {mid} # Delete immediately
# Invites (for web app access)
deadrop invite create {ns} {identity_id} # Create invite link
deadrop invite create {ns} {id} --name "Name" --expires-in 48h
deadrop invite list {ns} # List pending invites
deadrop invite revoke {ns} {invite_id} # Revoke an invite
# Server
deadrop serve # Run server
deadrop serve --no-auth # Development mode
deadrop serve --reload # With auto-reload
# Jobs (requires DB access)
deadrop jobs ttl # Process expired messages
deadrop jobs ttl --dry-run # Show what would be processed
deadrop jobs ttl --archive-path /path/to/archives
License
MIT
Metadata
Release files for deaddrop 0.5.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| deaddrop-0.5.3.tar.gz | 229.3 kB | With provenance | Details for deaddrop-0.5.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| deaddrop-0.5.3-py3-none-any.whl | Python 3 | none | any | Details for deaddrop-0.5.3-py3-none-any.whl |
Total release size: 340.5 kB
Release files / deaddrop-0.5.3.tar.gz
| Download URL | deaddrop-0.5.3.tar.gz |
|---|---|
| Size | 229.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4a3ecc4abf17313dcc3f899fd6066057b487dcd4c13b2ff57a1ea712d590161
|
|
BLAKE2b-256 checksum How to use checksums |
305b2568b49ed733998c99c8b1b04a582002b9581199d50061dda1fdb8465979
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 1, 2026.
Transparency logRelease files / deaddrop-0.5.3-py3-none-any.whl
| Download URL | deaddrop-0.5.3-py3-none-any.whl |
|---|---|
| Size | 111.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
63a1acdee6499003b9ac0c8b1348ff021659cd43a7c25ec1ad11896c990d287b
|
|
BLAKE2b-256 checksum How to use checksums |
0009605d8435dfe9ba2ed2bf9c89bd51281eb5db026fc3b50addb54dab467225
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 1, 2026.
Transparency logSecurity for deaddrop 0.5.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for deaddrop-0.5.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.