# agent email layer - deployment guide

## project structure

the agent email layer is organized under `src/synqed/agent_email/`:

```
src/synqed/agent_email/
├── __init__.py           # main exports
├── addressing.py         # agent id format (uri ⟷ email)
├── registry/
│   ├── models.py        # in-memory registry
│   ├── db.py            # postgres registry
│   └── api.py           # fastapi endpoints
├── inbox/
│   └── api.py           # a2a inbox endpoint
└── main.py              # fastapi application
```

this keeps the agent email functionality cleanly separated from the core synqed framework (agent.py, server.py, router.py, etc.).

this guide covers deploying the agent email layer as a public service where anyone can register agent addresses.

## architecture overview

### current (mvp) architecture
- **registry**: in-memory python dict
- **inbox routing**: local runtimes registered in same process
- **auth**: none (accepts all requests)
- **database**: none

### production architecture
```
┌─────────────────────────────────────────────────────────┐
│                    public internet                       │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│              load balancer / cdn (cloudflare)            │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│           agent email layer api (fastapi)                │
│  ┌──────────────────┐  ┌──────────────────┐             │
│  │  registry api    │  │   inbox api      │             │
│  │  /v1/agents      │  │   /v1/a2a/inbox  │             │
│  └──────────────────┘  └──────────────────┘             │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│              postgres database (registry)                │
│  - agent_registry table                                  │
│  - inbox_messages table (audit log)                      │
└─────────────────────────────────────────────────────────┘
```

### key changes needed for production

1. **persistent storage**: replace in-memory registry with postgres
2. **authentication**: implement api keys or oauth for agent registration
3. **remote inbox routing**: agents run on their own servers, registry only stores urls
4. **rate limiting**: prevent abuse
5. **monitoring**: observability and alerting
6. **https**: secure connections

## step 1: add postgres backend

### install dependencies

```bash
pip install sqlalchemy asyncpg psycopg2-binary alembic
```

### create database models

create `synqed/registry/db.py`:

```python
"""
postgres backend for agent registry.
"""

from datetime import datetime
from typing import List, Optional
from sqlalchemy import Column, String, DateTime, JSON, Index, create_engine
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from pydantic import HttpUrl

from synqed.registry.models import AgentRegistryEntry

Base = declarative_base()


class AgentRegistryDB(Base):
    """sqlalchemy model for agent registry."""
    
    __tablename__ = "agent_registry"
    
    # primary key is agent_id (canonical uri)
    agent_id = Column(String, primary_key=True)
    email_like = Column(String, unique=True, nullable=False, index=True)
    inbox_url = Column(String, nullable=False)
    public_key = Column(String, nullable=True)
    capabilities = Column(JSON, nullable=False, default=list)
    metadata = Column(JSON, nullable=False, default=dict)
    
    # audit fields
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
    
    # indexes
    __table_args__ = (
        Index('idx_email_like', 'email_like'),
        Index('idx_created_at', 'created_at'),
    )


class PostgresAgentRegistry:
    """
    postgres-backed agent registry.
    
    drop-in replacement for in-memory AgentRegistry.
    """
    
    def __init__(self, database_url: str):
        """
        initialize postgres registry.
        
        args:
            database_url: postgres connection string
                example: "postgresql+asyncpg://user:pass@localhost/agentdb"
        """
        self.engine = create_async_engine(database_url, echo=False)
        self.async_session = sessionmaker(
            self.engine, class_=AsyncSession, expire_on_commit=False
        )
    
    async def init_db(self) -> None:
        """create tables if they don't exist."""
        async with self.engine.begin() as conn:
            await conn.run_sync(Base.metadata.create_all)
    
    async def register(self, entry: AgentRegistryEntry) -> None:
        """register or update an agent."""
        async with self.async_session() as session:
            db_entry = AgentRegistryDB(
                agent_id=entry.agent_id,
                email_like=entry.email_like,
                inbox_url=str(entry.inbox_url),
                public_key=entry.public_key,
                capabilities=entry.capabilities,
                metadata=entry.metadata,
            )
            await session.merge(db_entry)  # upsert
            await session.commit()
    
    async def get_by_uri(self, agent_uri: str) -> AgentRegistryEntry:
        """lookup agent by canonical uri."""
        async with self.async_session() as session:
            result = await session.get(AgentRegistryDB, agent_uri)
            if result is None:
                raise KeyError(f"agent not found: {agent_uri}")
            
            return AgentRegistryEntry(
                agent_id=result.agent_id,
                email_like=result.email_like,
                inbox_url=HttpUrl(result.inbox_url),
                public_key=result.public_key,
                capabilities=result.capabilities,
                metadata=result.metadata,
            )
    
    async def get_by_email(self, email_like: str) -> AgentRegistryEntry:
        """lookup agent by email-like address."""
        from sqlalchemy import select
        
        async with self.async_session() as session:
            stmt = select(AgentRegistryDB).where(AgentRegistryDB.email_like == email_like)
            result = await session.execute(stmt)
            db_entry = result.scalar_one_or_none()
            
            if db_entry is None:
                raise KeyError(f"agent not found: {email_like}")
            
            return AgentRegistryEntry(
                agent_id=db_entry.agent_id,
                email_like=db_entry.email_like,
                inbox_url=HttpUrl(db_entry.inbox_url),
                public_key=db_entry.public_key,
                capabilities=db_entry.capabilities,
                metadata=db_entry.metadata,
            )
    
    async def list_all(self) -> List[AgentRegistryEntry]:
        """get all registered agents."""
        from sqlalchemy import select
        
        async with self.async_session() as session:
            stmt = select(AgentRegistryDB).order_by(AgentRegistryDB.created_at.desc())
            result = await session.execute(stmt)
            db_entries = result.scalars().all()
            
            return [
                AgentRegistryEntry(
                    agent_id=e.agent_id,
                    email_like=e.email_like,
                    inbox_url=HttpUrl(e.inbox_url),
                    public_key=e.public_key,
                    capabilities=e.capabilities,
                    metadata=e.metadata,
                )
                for e in db_entries
            ]
```

### update registry api to use postgres

modify `synqed/registry/api.py`:

```python
# at the top, add:
import os
from synqed.registry.db import PostgresAgentRegistry

# replace global registry with:
_global_registry = None

def get_registry():
    """get registry instance (postgres if DATABASE_URL set, else in-memory)."""
    global _global_registry
    
    if _global_registry is None:
        database_url = os.getenv("DATABASE_URL")
        if database_url:
            _global_registry = PostgresAgentRegistry(database_url)
        else:
            # fallback to in-memory for local dev
            from synqed.registry.models import AgentRegistry
            _global_registry = AgentRegistry()
    
    return _global_registry
```

## step 2: add authentication

### api key authentication

create `synqed/auth.py`:

```python
"""
authentication middleware for agent registration.
"""

import secrets
from typing import Optional
from fastapi import HTTPException, Security, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import Column, String, DateTime, Boolean
from datetime import datetime

from synqed.registry.db import Base


class APIKey(Base):
    """api key table."""
    
    __tablename__ = "api_keys"
    
    key = Column(String, primary_key=True)
    owner_email = Column(String, nullable=False)
    description = Column(String, nullable=True)
    is_active = Column(Boolean, default=True, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    last_used_at = Column(DateTime, nullable=True)


security = HTTPBearer()


async def verify_api_key(
    credentials: HTTPAuthorizationCredentials = Security(security)
) -> str:
    """
    verify api key from bearer token.
    
    returns owner_email if valid, raises 401 if invalid.
    """
    # todo: implement actual database lookup
    # for now, check against environment variable
    valid_key = os.getenv("AGENT_REGISTRY_API_KEY")
    
    if not valid_key or credentials.credentials != valid_key:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="invalid api key",
        )
    
    return "authenticated"


def generate_api_key() -> str:
    """generate a new api key."""
    return f"ak_{secrets.token_urlsafe(32)}"
```

### protect registration endpoint

modify `synqed/registry/api.py`:

```python
from synqed.auth import verify_api_key

@router.post(
    "",
    response_model=AgentRegistryEntry,
    status_code=status.HTTP_201_CREATED,
    summary="Register a new agent",
)
async def register_agent(
    request: AgentRegistrationRequest,
    owner: str = Depends(verify_api_key),  # add this
) -> AgentRegistryEntry:
    # ... rest of implementation
```

## step 3: deployment options

### option a: google cloud run (recommended)

**pros**: auto-scaling, pay-per-use, https by default, easy to deploy
**cons**: cold starts (but minimal for this use case)

#### dockerfile

create `Dockerfile`:

```dockerfile
FROM python:3.11-slim

WORKDIR /app

# install dependencies
COPY pyproject.toml ./
RUN pip install --no-cache-dir -e .

# copy source
COPY src/ ./src/
COPY demos/ ./demos/

# expose port
EXPOSE 8080

# set environment
ENV PORT=8080
ENV PYTHONUNBUFFERED=1

# run server
CMD uvicorn synqed.main:app --host 0.0.0.0 --port $PORT
```

#### deploy to cloud run

```bash
# set project
gcloud config set project YOUR_PROJECT_ID

# build and push image
gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/agent-email-layer

# deploy
gcloud run deploy agent-email-layer \
  --image gcr.io/YOUR_PROJECT_ID/agent-email-layer \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars DATABASE_URL="postgresql+asyncpg://..." \
  --set-env-vars AGENT_REGISTRY_API_KEY="your-secret-key" \
  --min-instances 1 \
  --max-instances 10
```

#### connect to cloud sql postgres

```bash
# create postgres instance
gcloud sql instances create agent-registry-db \
  --database-version=POSTGRES_15 \
  --tier=db-f1-micro \
  --region=us-central1

# create database
gcloud sql databases create agentdb --instance=agent-registry-db

# get connection string
gcloud sql instances describe agent-registry-db --format="value(connectionName)"

# deploy with cloud sql
gcloud run deploy agent-email-layer \
  --add-cloudsql-instances YOUR_PROJECT:us-central1:agent-registry-db \
  --set-env-vars DATABASE_URL="postgresql+asyncpg://user:pass@/agentdb?host=/cloudsql/YOUR_PROJECT:us-central1:agent-registry-db"
```

### option b: aws ec2 + rds

```bash
# launch ec2 instance
aws ec2 run-instances \
  --image-id ami-0c55b159cbfafe1f0 \
  --instance-type t3.small \
  --key-name your-key-pair

# create rds postgres
aws rds create-db-instance \
  --db-instance-identifier agent-registry-db \
  --db-instance-class db.t3.micro \
  --engine postgres \
  --master-username admin \
  --master-user-password yourpassword \
  --allocated-storage 20

# on ec2 instance:
git clone <your-repo>
cd synqed-python
pip install -e .
export DATABASE_URL="postgresql+asyncpg://admin:pass@your-rds.amazonaws.com/agentdb"
export AGENT_REGISTRY_API_KEY="your-secret-key"
uvicorn synqed.main:app --host 0.0.0.0 --port 80
```

### option c: fly.io (simplest)

create `fly.toml`:

```toml
app = "agent-email-layer"
primary_region = "sjc"

[build]
  builder = "paketobuildpacks/builder:base"

[env]
  PORT = "8080"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1

[[services]]
  protocol = "tcp"
  internal_port = 8080

  [[services.ports]]
    port = 80
    handlers = ["http"]

  [[services.ports]]
    port = 443
    handlers = ["tls", "http"]
```

deploy:

```bash
# install fly cli
curl -L https://fly.io/install.sh | sh

# login
fly auth login

# create app
fly apps create agent-email-layer

# create postgres
fly postgres create --name agent-registry-db

# attach to app
fly postgres attach --app agent-email-layer agent-registry-db

# set secrets
fly secrets set AGENT_REGISTRY_API_KEY="your-secret-key"

# deploy
fly deploy
```

## step 4: production configuration

### environment variables

```bash
# database
DATABASE_URL=postgresql+asyncpg://user:pass@host/db

# authentication
AGENT_REGISTRY_API_KEY=ak_your_secret_key_here

# rate limiting
RATE_LIMIT_PER_MINUTE=60

# cors
ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com

# monitoring
SENTRY_DSN=https://...
LOG_LEVEL=INFO
```

### add rate limiting

install:
```bash
pip install slowapi
```

in `synqed/main.py`:

```python
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# add to endpoints:
@router.post("")
@limiter.limit("10/minute")  # 10 registrations per minute
async def register_agent(...):
    ...
```

### add monitoring

```bash
pip install sentry-sdk
```

in `synqed/main.py`:

```python
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration

if os.getenv("SENTRY_DSN"):
    sentry_sdk.init(
        dsn=os.getenv("SENTRY_DSN"),
        integrations=[FastApiIntegration()],
        traces_sample_rate=0.1,
    )
```

## step 5: dns and domain

### setup custom domain

1. register domain (e.g., `agentmail.io`)
2. point to your deployment:
   - **cloud run**: add custom domain in console
   - **ec2**: point a record to elastic ip
   - **fly.io**: `fly certs add agentmail.io`

### example dns records

```
agentmail.io          A      35.201.xxx.xxx
api.agentmail.io      CNAME  agent-email-layer.run.app
```

## step 6: client sdk

create a python sdk for easy agent registration:

```python
"""
client sdk for agent email layer.
"""

import httpx
from pydantic import HttpUrl

class AgentMailClient:
    """client for agent email layer api."""
    
    def __init__(self, api_key: str, base_url: str = "https://api.agentmail.io"):
        self.api_key = api_key
        self.base_url = base_url
        self.client = httpx.AsyncClient(
            headers={"Authorization": f"Bearer {api_key}"}
        )
    
    async def register(
        self,
        email_like: str,
        inbox_url: str,
        capabilities: list[str] = None,
        metadata: dict = None,
    ):
        """register your agent."""
        response = await self.client.post(
            f"{self.base_url}/v1/agents",
            json={
                "email_like": email_like,
                "inbox_url": inbox_url,
                "capabilities": capabilities or [],
                "metadata": metadata or {},
            },
        )
        response.raise_for_status()
        return response.json()
    
    async def lookup(self, email: str):
        """lookup an agent by email."""
        response = await self.client.get(
            f"{self.base_url}/v1/agents/by-email/{email}"
        )
        response.raise_for_status()
        return response.json()
    
    async def send_message(
        self,
        from_agent: str,
        to_email: str,
        message: dict,
    ):
        """send a2a message to an agent."""
        # lookup recipient
        recipient = await self.lookup(to_email)
        
        # send to inbox
        response = await self.client.post(
            recipient["inbox_url"],
            json={
                "sender": from_agent,
                "recipient": recipient["agent_id"],
                "message": message,
            },
        )
        response.raise_for_status()
        return response.json()
```

## example usage (after deployment)

```python
# register your agent
client = AgentMailClient(api_key="ak_your_key")

await client.register(
    email_like="my-agent@myorg",
    inbox_url="https://myagent.example.com/inbox",
    capabilities=["a2a/1.0", "code-generation"],
)

# send message to another agent
await client.send_message(
    from_agent="agent://myorg/my-agent",
    to_email="gemini@google",
    message={
        "thread_id": "abc123",
        "role": "user",
        "content": "help me debug this code",
    },
)
```

## cost estimates

### google cloud run + cloud sql
- **cloud run**: ~$0-20/month (depends on traffic)
- **cloud sql (micro)**: ~$7-10/month
- **total**: ~$10-30/month for thousands of requests

### fly.io
- **app**: $0-5/month (shared cpu)
- **postgres**: $0-15/month (small instance)
- **total**: ~$5-20/month

### aws ec2 + rds
- **t3.small**: ~$15/month
- **rds t3.micro**: ~$15/month
- **total**: ~$30/month

## security checklist

- [ ] enable https (tls/ssl)
- [ ] implement api key authentication
- [ ] add rate limiting
- [ ] validate inbox urls (prevent ssrf)
- [ ] sanitize user inputs
- [ ] setup cors properly
- [ ] enable request logging
- [ ] add monitoring/alerting
- [ ] backup database regularly
- [ ] implement key rotation
- [ ] add ddos protection (cloudflare)

## next steps

1. **implement postgres backend** (see db.py above)
2. **add authentication** (api keys)
3. **create dockerfile**
4. **deploy to cloud run** (or fly.io for simplicity)
5. **setup postgres database**
6. **configure domain and https**
7. **build client sdk**
8. **add monitoring**
9. **write documentation**
10. **launch! 🚀**

---

**questions?** open an issue or contact the maintainers.

