Groundwork for the daily activity digest: give the CRM an outbound mail path. Today nothing leaves the box (Gmail capture + drafts only), so this adds a dedicated, per-package SMTP account independent of any StartOS system-wide SMTP. - configureDigestSmtp Start9 action: writes host/port/from/username/password/ security to /data/secrets/smtp/* (password piped over stdin, never argv/env; per-field files, owner-only) — mirrors the setAnthropicApiKey pattern. - docker_entrypoint.sh reads those at boot and exports SMTP_* (operator env wins). - backend/smtp_send.py: stdlib smtplib wrapper reading SMTP_* (one code path for dev .env and the box); starttls/tls/none modes. - POST /api/admin/digest/test-email (admin-only): proves the pipe. Recipients are restricted to the active-admin set — an arbitrary `to` is rejected, so the endpoint is not an open relay; send failures are logged, not echoed (an SMTP auth error can carry the credential). - Tests: test_smtp_send.py (sender), test_smtp_endpoint.py (gating + relay restriction + no-leak). 18/18 backend green; s9pk typechecks. Analysis/summarization for the digest body (Phase B) will run on Spark, never Claude — the digest is deliberately un-anonymized. Decisions + Phase B plan in ROADMAP.md.
8.5 KiB
Venture CRM Roadmap (Airtable Replacement)
Current status
- Premium Airtable-like frontend grid exists and is actively iterating.
- Backend now has production-grade APIs for:
GET /api/fundraising/statePUT /api/fundraising/state(with optimistic version check)GET /api/fundraising/exportPOST /api/fundraising/backupPOST /api/fundraising/restore-previewPOST /api/fundraising/restoreGET /api/fundraising/backupsGET/PATCH /api/fundraising/backup-policyGET /api/fundraising/relational-summaryGET /api/feature-requestsPOST /api/feature-requestsPATCH /api/feature-requests/:id
- New DB tables:
fundraising_statefundraising_investorsfundraising_contactsfundraising_fundsfundraising_commitmentsfundraising_viewsfeature_requestsapp_settings
- Grid saves/restores now sync into relational fundraising tables automatically.
- Formula engine is now sandboxed (no
eval/new Function) with expanded function support. - Automation engine v1 added:
- Rule table + toggle API
- List memberships (
main,follow_up,graveyard,longshot,all) - Automation run log
- Collaboration/reliability additions:
- Unified activity feed API (
audit+automation+backup) - Backup integrity verification API
- Better version-conflict metadata (
updated_at,updated_by)
- Unified activity feed API (
- Security hardening additions:
- Basic IP rate limiting (login and write APIs)
- Configurable CORS origin (
CRM_CORS_ORIGIN) - Production secret enforcement (
CRM_ENV=productionrequiresCRM_SECRET_KEY) - Security status API + go-live checklist (
SECURITY.md)
Phase 1 (Production foundation)
- Persist grid + views on backend
- Wire frontend fundraising grid reads/writes to
/api/fundraising/state. - Keep localStorage only as emergency fallback.
- Add autosave debounce and conflict handling (
expected_version).
- Admin-invite auth model
- Disable self-register for non-admin users.
- Add admin-only invite/create-user endpoint.
- Keep role model:
admin,member.
- Deployment and remote access
- Add
docker-composefor one-command launch. - Reverse proxy + TLS option (Caddy/Traefik) for non-Tailscale deployments.
- Recommended for your use case: Tailscale private access to laptop host.
- Data safety and operations
- Automated nightly SQLite backups and restore test script.
- Add
/api/fundraising/exportfor JSON snapshot export. - Add health/readiness checks.
Phase 2 (Airtable parity)
- Advanced views
- Multi-condition filter groups (AND/OR groups)
- Multi-column sorting
- Pinned/frozen columns
- Personal vs shared views
- Formula engine v2
- Add functions:
SUM,MIN,MAX,ROUND,ABS,CONCAT(done) - Type-aware formulas and better errors
- Dependency graph and recalculation rules
- Activity + audit
- Record-level change history in UI
- Last modified by / at fields
- Restore archived rows
Phase 3 (Team workflow and automation)
- Tasks/reminders tied to investors/contacts
- Automation rules (graveyard/follow-up triggers)
- Email/communication integrations (optional)
- Granular permissions (if team grows)
Backlog (post-Phase-1 agentic)
Daily activity digest (email to the team)
Requested 2026-06-15. Phase A built in v0.1.0:75 (outbound SMTP send capability + admin test-email endpoint; not yet deployed). Phase B (digest content + Spark summarization + daily scheduler) remains.
Decisions (locked 2026-06-15): recipients = all active admins; summarization = Spark-LLM narrative (never Claude — un-anonymized substance stays local); granularity = grouped by user (→ per investor).
Phase A — DONE (v0.1.0:75): configureDigestSmtp Start9 action writes a per-package SMTP account to /data/secrets/smtp/*; docker_entrypoint.sh exports SMTP_*; backend/smtp_send.py (stdlib smtplib) + admin POST /api/admin/digest/test-email (recipient-restricted to the admin set — not an open relay). Tests: test_smtp_send.py, test_smtp_endpoint.py.
Phase B — TODO: daily scheduler (co-locate with email_integration/scheduler.py); per-user→per-investor activity query (deleted_at IS NULL throughout); Spark-narrative summary of captured email substance; compose + send to all admins.
Have the CRM send a daily digest email summarizing each registered user's activity — primarily who emailed which investors and the substance of those emails — to the fund principal (and eventually other admins). Scales with the synced-user count: 2 users synced today, ~5 eventually.
- Source data: the captured email-activity already flowing through the Gmail DWD propose→approve pipeline (
backend/email_integration/), keyed per registered user → per investor/contact. Optionally fold in other CRM activity (audit feed, automation runs, new opportunities) later. - Send path is NEW capability. Today nothing leaves the box — the system only captures Gmail and creates drafts. This needs outbound SMTP. StartOS 0.4 has a system-wide SMTP account (since v0.4.0-beta.9): the user configures it once for the whole server and services read it via
sdk.getSystemSmtp(effects).const(), which returns aT.SmtpValue(host,port,from,username,password,security). Wire the digest sender to that rather than hardcoding any account. Implementation path (researched 2026-06-15, our SDK pin^0.4.0-beta.66): model amanageSMTPaction on gitea-startos / vaultwarden-startos — a three-wayselection(system / custom / disabled) built onsdk.inputSpecConstants.smtpInputSpec, persisted tostoreJson, withmain.tsinjectingSMTP_HOST/PORT/USER/PASS/FROM/SECURITYenv vars into the daemonexecblock (same shape as the existingsetAnthropicApiKey.tsaction). The Python sender reads them viaos.environand openssmtplib.SMTP/SMTP_SSL. "Custom SMTP" is a dedicated per-package account, fully independent of the server's system SMTP — the custom branch never callsgetSystemSmtp, so the digest can send through its own provider even on a box with no system account configured (confirmed in both reference packages). This is the likely fit here: a digest-only mailbox separate from anyone's Gmail. Note StartOS 0.4 dropped the oldConfig/Propertiesmanifest spec — SMTP config is an action + storeJson, not a manifest config field. SDK note (verified 2026-06-15): our pin^0.4.0-beta.66resolves to exactly0.4.0-beta.66(caret on a prerelease stays within the0.4.0tuple), whose SMTP surface —getSystemSmtp→T.SmtpValue {host, port, from, username, password, security},inputSpecConstants.smtpInputSpec(providers gmail/ses/sendgrid/mailgun/protonmail/other; selection disabled/system/custom),smtpShape,smtpPrefill— is byte-identical to the 1.5.3 reference packages (verified from published tarballs; reponode_modulesis absent). Build against beta.66 as-is — no SDK bump needed (moving to 1.x is a major-track change with broad blast radius acrossstartos/, and nothing about SMTP justifies it). - Analysis runs on Spark, never Claude. The digest is deliberately un-anonymized (real LP names + email substance), so any summarization/analysis must go through Spark Control to local models — this is the one path that intentionally bypasses the scrub→Claude→re-hydrate boundary, because keeping the substance local is the whole point. Never route digest content to Claude.
- Exempt from "agents draft, humans send." That rule governs outward LP/prospect contact. This is an internal ops digest to the team's own inboxes — a different category — so an automated daily send here does not violate the draft-only guardrail. State this explicitly at build time.
- Scheduling: a daily cron, naturally co-located with the existing
backend/email_integration/scheduler.pysync cadence. - Soft-delete: every aggregate/read in the digest must filter
deleted_at IS NULL(see the standing soft-delete rule).
Open design questions (Phase B detail, still to settle): fixed daily send time; "nothing happened today" suppression; whether the Spark summary is per-investor-thread or a single per-user narrative.
Definition of done for "Airtable substitute" v1
- Team can manage all investors in one master table
- Saved views replicate current Airtable workflows
- CSV import from Airtable is reliable and repeatable
- Data persists safely and supports multi-user access
- Auth is invite-only and backups are automated