Smart Advisor OS
Smart Advisor OS sits across existing RIA tools — CRM, portfolio management, compliance, and custodian portals — and provides cross-system intelligence that no individual tool can offer alone. The core value proposition is converting raw meeting audio into structured notes, action items, client facts, compliance flags, follow-up emails, and CRM entries automatically.
| Layer | Technology |
|---|---|
| API Framework | FastAPI + uvicorn |
| Language | Python 3.12 |
| Database | PostgreSQL 17 (alpine) |
| ORM | SQLAlchemy 2.0 async (asyncpg) |
| Migrations | Alembic |
| Task Queue | Celery + Redis broker |
| Scheduler | Celery Beat (PersistentScheduler) |
| Cache / Broker | Redis 7 (alpine) |
| Transcription | AssemblyAI (default) · Deepgram (optional) |
| AI | Anthropic claude-sonnet-4-6 |
| Encryption | Fernet / MultiFernet (key rotation) |
| Passwords | bcrypt |
| Storage | AWS S3 (uploads + WORM) |
| SMTP (STARTTLS / SSL) | |
| Reverse Proxy | Caddy 2 (auto HTTPS via Let's Encrypt) |
Source Layout
src/smartad/ ├── api/ │ ├── main.py # app setup, middleware │ ├── ratelimit.py # Redis rate limiter │ └── routes/ # 25 route modules ├── auth/ │ ├── security.py # JWT + bcrypt │ └── dependencies.py # bearer token guard ├── db/ │ ├── models.py # 30+ ORM models │ ├── repository.py # data access layer │ ├── session.py # AsyncSession factory │ └── crypto.py # Fernet column types ├── worker/ │ ├── app.py # Celery config + Beat │ └── tasks.py # 7 async tasks ├── intelligence/ # 16 Claude-powered modules ├── crm/ # Wealthbox / Redtail / SF ├── transcription/ # AssemblyAI / Deepgram ├── archive/ # S3 WORM archival ├── storage/ # S3 upload presigning ├── notifications/ # SMTP briefing emails ├── documents/ # PDF/DOCX/CSV parsing ├── integrations/ # Teams + Google Meet ├── ssrf.py # SSRF protection └── config.py # Pydantic settings
| Service | Image | Port | Command | Role |
|---|---|---|---|---|
| api | smartad-api (Dockerfile) | 172.18.0.1:8000 | uvicorn … --reload | FastAPI HTTP server; only service reachable from Caddy proxy |
| worker | smartad-api | — | celery worker --concurrency=4 | Processes meeting pipeline, CRM sync, archive, document extraction |
| beat | smartad-api | — | celery beat | Cron scheduler — fires daily morning briefing task |
| postgres | postgres:17-alpine | 5432 (internal) | entrypoint | Primary datastore; health-checked before dependents start |
| redis | redis:7-alpine | 6379 (internal) | entrypoint | Celery broker, result backend, rate-limit counters |
Network Topology
The API port is bound exclusively to 172.18.0.1 (the Docker bridge gateway used by Caddy). It is not exposed on any public interface.
Every meeting follows the same async pipeline. The HTTP request returns immediately after saving the record; all heavy work happens in the Celery worker.
Claude Extraction Schema
Claude is called with structured tool use, guaranteeing typed output:
All endpoints except webhooks are versioned under /api/v1/. Webhooks are at the root path.
JWT + Stateful Refresh Tokens
| Token | Detail |
|---|---|
| Access token | HS256 JWT · 15 min TTL · claims: sub, exp, iss, aud |
| Refresh token | 32-byte random hex · 30-day TTL · SHA-256 hashed in DB |
| Issuer / Audience | smartbuzzai |
| Password hash | bcrypt (automatic cost factor) |
Security Properties
Rate Limits (Redis)
| Endpoint | Limit | Window | Key |
|---|---|---|---|
| POST /login | 5 | 60 s | client IP |
| POST /register | 5 | 60 s | client IP |
| AI routes | 20 | 60 s | user_id |
| Task | Retries | Delay | Trigger | Description |
|---|---|---|---|---|
| process_meeting | 3 | 30 s | API / webhooks | Full pipeline: transcribe → Claude extraction → archive + CRM sync |
| archive_meeting | 3 | 60 s | process_meeting | Serialise meeting JSON → S3 Object Lock COMPLIANCE (6-yr retention) |
| sync_meeting_to_crm | 3 | 60 s | process_meeting | Build CRMNote + CRMTask list → push to all active providers in parallel |
| fetch_teams_recording | 5 | 120 s | /webhooks/teams | Poll Microsoft Graph for recording URL (up to ~2 hrs), create Meeting |
| fetch_meet_recording | 5 | 120 s | /webhooks/meet | Fetch Google Drive download URL via Meet API + service account impersonation |
| send_morning_briefings | — | Beat cron | Daily (configurable time) | Build DailyBriefing per active user (semaphore=10), batch-send SMTP |
| process_document | 3 | 30 s | POST /documents | Extract facts + summary from PDF/DOCX/CSV/TXT via Claude tool use |
All models carry a user_id foreign key enforcing row-level multi-tenancy. Sensitive columns use custom SQLAlchemy types that transparently encrypt/decrypt via Fernet.
| Model | Phase | Key Fields | Notes |
|---|---|---|---|
| User | Core | id, email, hashed_password, is_active | Root tenant anchor |
| RefreshToken | Core | token_hash (SHA-256), expires_at, revoked | Stateful revocation |
| Meeting | 1 | status, recording_url, consent_given, raw_transcript (enc), notes, action_items, client_facts, follow_up_email, compliance_flags, archive_key | Append-only by convention |
| ClientProfile | 1 | client_name, aum (Numeric 15,2), annual_fee_rate | Unique per user + name |
| Referral | 2 | referrer_client, prospect_name, status, expected_aum, actual_aum | Growth pipeline tracking |
| Family | 3 | name, primary_client_name, estimated_total_aum | Wealth transfer root |
| FamilyMember | 3 | generation (0=primary, 1=children…), birth_year, has_independent_advisor, estimated_aum | CASCADE from Family |
| ValuesProfile | 3 | growth_orientation, risk_appetite, social_impact, family_legacy, philanthropy, liquidity_preference (all 1–10) | CASCADE from FamilyMember |
| TransferMilestone | 3 | category, due_date, completed_at, status | CASCADE from Family |
| HeirEngagementEvent | 3 | event_type, event_date | CASCADE from FamilyMember |
| ComplianceDocument | 4 | document_type, generated_content, status, regulatory_citations | policy · ADV · annual_review |
| MarketingReview | 4 | content_type, is_compliant, issues, suggestions | AI-reviewed marketing copy |
| ExamReadinessAssessment | 4 | overall_score, domain_scores, deficiencies | Series 65/66 readiness |
| CareerLadder | 4 | stages (JSON), benchmarks (JSON) | AI-generated advisor ladder |
| CompPlan | 4 | base_range, bonus_structure, equity_structure, kpis, market_comparison | AI-generated comp plan |
| NextGenAssessment | 4 | overall_score, dimension_scores, development_plan, transition_timeline_months | Successor readiness |
| ChatSession / ChatMessage | 5 | role (user|assistant), content | Unified AI assistant history |
| PrioritySnapshot | 5 | priorities (JSON), focus_recommendation | Daily AI priority list |
| DetectedOpportunity | 5 | opportunity_type, priority_score (0–100), is_dismissed | 529, drift, RMD, fee anomaly… |
| FirmHealthPulse | 5 | client_count, meeting_count, action_completion_rate, metrics, recommendations, ai_narrative | Periodic practice health report |
| CrmConnection | CRM | provider, credentials (EncryptedJSON), is_active | Unique per user + provider |
| ClientDocument | Docs | filename, raw_text (EncryptedText), extracted_facts, summary, status | PDF/DOCX/CSV/TXT intel |
| Version | Description |
|---|---|
| 4ee95c27aba9 | Initial schema — users, meetings, clients |
| 95486cc61302 | Add referrals table |
| h8i9j0k1l2m3 | Add refresh_tokens table |
| c4d5e6f7a8b9 | Add client_profiles |
| c335ca5ff915 | Phase 3 — Family, FamilyMember, ValuesProfile, TransferMilestone, HeirEngagementEvent |
| d4e5f6a7b8c9 | Phase 4 — ComplianceDocument, MarketingReview, ExamReadinessAssessment |
| e5f6a7b8c9d0 | Phase 4 — CareerLadder, CompPlan, NextGenAssessment |
| f6a7b8c9d0e1 | Phase 5 — ChatSession, ChatMessage, PrioritySnapshot, DetectedOpportunity, FirmHealthPulse |
| a1b2c3d4e5f6 | CRM connections table |
| b2c3d4e5f6a7 | client_documents table |
| b3c9e1f2a4d8 | WORM archive fields (archived_at, archive_key) on Meeting |
| g7h8i9j0k1l2 | Encrypt CRM credentials at rest (EncryptedJSON) |
| i9j0k1l2m3n4 | Encrypt raw_transcript in meetings (EncryptedText) |
| 31c2dc772247 | Encrypt client_document raw_text and chat_messages |
| 5af6d4bfc26c | Status indexes on meetings + related tables |
| 89472f1a45ce | Status indexes on referrals + related tables |
| j0k1l2m3n4o5 | Check constraints — scores (0–100), birth_year (1900–2100) |
| f23ff3b0714f | Merge / fix priority_score constraint |
| k1l2m3n4o5p6 | Check constraint on opportunity priority_score |
| 11f896f1bda0 | Make meeting.user_id NOT NULL with CASCADE delete |
| 3bbdfbbcfc6d | Change client_document status column to enum |
Encrypted Columns
| Model | Column | Type |
|---|---|---|
| Meeting | raw_transcript | EncryptedText |
| ClientDocument | raw_text | EncryptedText |
| CrmConnection | credentials | EncryptedJSON |
| ChatMessage | content | EncryptedText |
Key Rotation
CREDENTIALS_ENCRYPTION_KEY accepts comma-separated Fernet keys. The first key encrypts new data; all keys are tried for decryption — enabling zero-downtime rotation.
# Rotate: prepend new key, restart, re-encrypt rows, remove old key CREDENTIALS_ENCRYPTION_KEY=new_key,old_key
Decryption failures are surfaced as exceptions rather than silently returning None — data corruption or key mismatch is never hidden.
All AI features use claude-sonnet-4-6 via structured tool use for guaranteed typed output. Prompt caching is applied to static system prompts to reduce latency and cost.
Transcription is abstracted behind a TranscriptionProvider protocol in src/smartad/transcription/base.py. New providers slot in without touching the pipeline.
TRANSCRIPTION_PROVIDER=deepgram + DEEPGRAM_API_KEY to activate| Platform | Endpoint | Validation | Recording Fetch | Consent |
|---|---|---|---|---|
| Zoom | /webhooks/zoom | HMAC-SHA256 signature + 300 s timestamp drift check | Direct URL from payload (SSRF-checked) | consent_given=False until advisor approves |
| Microsoft Teams | /webhooks/teams | clientState secret header | Graph API → organizer's OneDrive (async retry up to ~2 hrs) | consent_given=False |
| Google Meet | /webhooks/meet | Token in URL path (Pub/Sub push secret) | Drive API with service account impersonation | consent_given=False |
All platforms set consent_given=False on creation. The advisor must explicitly grant consent before the meeting enters processing. This prevents cross-account injection even if webhook HMAC validation is bypassed.
Sync creates a CRMNote (subject + body from meeting notes) and a list of CRMTask objects (title, due date, assignee) per action item. Contact lookup is by client name. All active connections sync in parallel; failures per provider are logged but don't fail the others.
Upload Bucket
| Setting | Value |
|---|---|
| Env var | UPLOAD_BUCKET_NAME |
| Path scheme | uploads/{year}/{month}/{uuid}.{ext} |
| Presign TTL | 7 days (configurable) |
| Max file size | 500 MB (configurable) |
| Formats | MP3 M4A MP4 WAV OGG FLAC AAC WEBM |
WORM Compliance Archive
| Setting | Value |
|---|---|
| Env var | WORM_BUCKET_NAME |
| Path scheme | meetings/{year}/{month}/{meeting_id}.json |
| Retention | 2190 days (6 years) · SEC Rule 17a-4 |
| Lock mode | S3 Object Lock COMPLIANCE (irrevocable) |
| Content | Full meeting JSON excluding encrypted transcript |
If archival permanently fails after all retries, a COMPLIANCE ACTION REQUIRED error is logged — the failure is surfaced rather than swallowed.
| Layer | Implementation | Detail |
|---|---|---|
| CORS | FastAPI CORSMiddleware | Origins from CORS_ALLOWED_ORIGINS; warns if empty (blocks all cross-origin) |
| Rate Limiting | Redis Lua atomic counters | 5/min on auth; 20/min on AI routes; HTTP 429 + Retry-After |
| SSRF Protection | ssrf.py · is_safe_url() |
Blocks non-HTTPS + private/loopback IPs; resolves hostnames via DNS — all results checked |
| Webhook Validation | HMAC-SHA256 (Zoom); clientState (Teams); URL token (Meet) | 300 s timestamp drift window; consent gate as secondary defence |
| Data Encryption | Fernet / MultiFernet | Transcripts, CRM credentials, documents, chat messages — never plaintext in DB |
| Port Isolation | docker-compose port binding | API bound to 172.18.0.1:8000 only — not reachable from public interfaces |
| Global Error Handler | FastAPI exception handler | Logs full traceback; returns generic HTTP 500 — no internal detail leaked |
| Audit Trail | Meeting rows append-only | Convention: never DELETE meetings — use status fields + WORM archive |
| Variable | Required | Default | Description |
|---|---|---|---|
| DATABASE_URL | yes | — | PostgreSQL asyncpg connection string |
| REDIS_URL | yes | — | Redis broker + result backend |
| ANTHROPIC_API_KEY | yes | — | Claude API key |
| JWT_SECRET | yes | — | ≥32 chars; HS256 signing key |
| CREDENTIALS_ENCRYPTION_KEY | yes | — | Fernet key(s), comma-separated for rotation |
| CORS_ALLOWED_ORIGINS | rec. | "" | Comma-separated allowed origins |
| ASSEMBLYAI_API_KEY | rec. | — | Required if TRANSCRIPTION_PROVIDER=assemblyai |
| TRANSCRIPTION_PROVIDER | no | assemblyai | assemblyai or deepgram |
| WORM_BUCKET_NAME | no | — | S3 bucket for compliance archive |
| WORM_OBJECT_LOCK | no | false | Set true in production for immutable retention |
| WORM_RETENTION_DAYS | no | 2190 | 6 years default (SEC Rule 17a-4) |
| UPLOAD_BUCKET_NAME | no | — | S3 bucket for user-uploaded audio/video |
| SMTP_HOST / PORT / USERNAME / PASSWORD / FROM | no | — | SMTP config for daily briefing emails |
| BRIEFING_SEND_HOUR / MINUTE / TIMEZONE | no | 6:30 ET | Daily briefing send schedule |
| ZOOM_WEBHOOK_SECRET_TOKEN | no | — | Zoom HMAC validation secret |
| TEAMS_TENANT_ID / CLIENT_ID / CLIENT_SECRET | no | — | Azure AD app for Teams Graph API |
| GOOGLE_SERVICE_ACCOUNT_JSON | no | — | Service account for Google Meet + Drive |
| TRUSTED_PROXY_COUNT | no | 0 | Reverse proxy hops for real IP extraction |