An open API service indexing awesome lists of open source software.

https://github.com/crodorg/infrasaas

Agnostic outreach + CRM + automation infrastructure. Rust workspace, HTTP API, terminal CRM.
https://github.com/crodorg/infrasaas

automation axum crm openbsd outreach postgres ratatui rust self-hosted sqlx tokio tui

Last synced: about 1 month ago
JSON representation

Agnostic outreach + CRM + automation infrastructure. Rust workspace, HTTP API, terminal CRM.

Awesome Lists containing this project

README

          

# infrasaas

[![CI](https://github.com/crodorg/infrasaas/actions/workflows/ci.yml/badge.svg)](https://github.com/crodorg/infrasaas/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-orange.svg)](Cargo.toml)
[![Rust 2024](https://img.shields.io/badge/edition-2024-dea584.svg)](https://doc.rust-lang.org/edition-guide/rust-2024/)

Agnostic outreach + CRM + automation infrastructure. Lead sourcing, enrichment, outreach, terminal CRM, and orchestration in one Rust workspace, exposed over an HTTP REST API. Build once, plug any SaaS in via HTTP.

**Status:** early Phase 0. Scaffolding only. APIs unstable.

## Design

- **Anti-fragility primary.** Direct dependencies are MIT or MIT/Apache-2.0 (true OSI). No "sustainable use" or source-available licenses. Every third-party wrapped behind a trait so it is swappable.
- **Privacy by default.** Self-hosted: own VPS, own Postgres, single static binaries, no telemetry.
- **Project-agnostic.** A `source` discriminator on the `leads` row routes data per consumer. No domain logic baked into the infra.
- **Terminal-native CRM.** ratatui TUI binary. No web UI in the critical path.
- **`unsafe_code = forbid`** at the workspace level.
- **Pure-Rust crypto.** TLS via `rustls` + `ring` (no OpenSSL, no `aws-lc-sys` C/asm). Avoids the C build-system blast radius on OpenBSD and minimizes supply-chain surface.
- **Secrets are never plain `String`.** API keys and connection URLs are wrapped in `secrecy::SecretString` to block accidental `Debug` / log leaks.

## Workspace

| Crate | Purpose |
|-------|---------|
| [`core`](crates/core) | Shared types: `Lead`, `Campaign`, `Integration` trait, errors |
| [`api`](crates/api) | axum HTTP server. REST endpoints for leads CRUD |
| [`crm`](crates/crm) | ratatui TUI. 4-screen CRM (list / detail / action / template-picker) |
| [`integrations/claude_enrich`](crates/integrations/claude_enrich) | Claude API enrichment |

Phase 0 ships the four crates above. Orchestrator, scrapers, email, and WhatsApp integrations are deferred to Phase 1.

## Stack

Rust 2024 · tokio · axum · sqlx · ratatui · reqwest · serde · tracing · jiff. Postgres 16+. OpenBSD-friendly single static binaries.

## Quickstart

Requires Rust 1.85+ and a local Postgres 16+.

```sh
git clone https://github.com/crodorg/infrasaas.git
cd infrasaas
cp .env.example .env

./scripts/dev-up.sh # boots a user-local Postgres on :5433, applies migrations, starts the API on :8080
```

In another terminal:

```sh
cargo run -q -p infrasaas-crm # ratatui CRM
```

Insert a lead:

```sh
curl -sX POST http://127.0.0.1:8080/leads \
-H 'content-type: application/json' \
-d '{"source":"example","name":"Ada Lovelace","email":"ada@example.com","tags":["beta"]}'
```

List leads:

```sh
curl -s http://127.0.0.1:8080/leads | jq
```

Tear down:

```sh
./scripts/dev-down.sh
```

## Build

```sh
cargo check --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo build --workspace --release
```

The `claude_enrich` crate ships a live integration test (`cargo test -p infrasaas-claude-enrich --test live`) that self-skips when `ANTHROPIC_API_KEY` is unset. CI runs it with the secret on pushes to `main`; forks and untrusted PRs see an empty value and skip cleanly.

## API

Phase 0 endpoints (more in subsequent phases):

| Method | Path | Body / Query | Returns |
|--------|------|--------------|---------|
| `POST` | `/leads` | `NewLead` JSON | `Lead` |
| `GET` | `/leads` | `?source=&status=&limit=` | `Vec` |
| `GET` | `/leads/{id}` | — | `Lead` |
| `PATCH` | `/leads/{id}` | `LeadPatch` JSON | `Lead` |
| `POST` | `/leads/{id}/enrich` | `{prompt_template, max_tokens?, model?}` | `Lead` (with `enriched_data`) |
| `GET` | `/healthz` | — | `200 OK` (process liveness) |
| `GET` | `/readyz` | — | `{"db":bool,"enrich":bool}` (200 ready / 503 not) |

### Enrichment

Enrichment requires `ANTHROPIC_API_KEY` in the API server's environment. The endpoint is project-agnostic — caller supplies the prompt template per-request, with `{{context}}` interpolated to the serialized lead. Without the key set, the API boots cleanly but `/enrich` returns `503 Service Unavailable`.

Default model is `claude-haiku-4-5-20251001` (cheap, fast — typical enrich call ~$0.003). Override globally via `ANTHROPIC_MODEL` env, or per-request via the `model` field for high-value leads where you want Sonnet 4.6 (`claude-sonnet-4-6`) or Opus 4.7 (`claude-opus-4-7`).

The CRM ships a small set of built-in prompt templates (`summary`, `opener`, `research`). To customize, copy [`templates.toml.example`](templates.toml.example) to `templates.toml` (gitignored), `$XDG_CONFIG_HOME/infrasaas/templates.toml`, or any path pointed at by the `INFRASAAS_TEMPLATES` env var. The CRM picker (Action screen → `e`) lists whatever it loads at boot.

### CRM keymap

Vim-style modal TUI. Three modes: NORMAL (default), CMD (`:`), SEARCH (`/`).

**Movement**

| key | effect |
|-----|--------|
| `j` / `k` | next / previous lead in current view |
| `gg` / `G` | jump to top / bottom |
| `Ctrl-d` / `Ctrl-u` | half-page down / up |
| `h` / `Esc` | back one screen |
| `l` / `Enter` | forward one screen |
| `q` | quit |

**List screen — quick status flip** (selection stays put, sweep through a batch)

| key | sets status to |
|-----|----------------|
| `c` | contacted |
| `R` | replied |
| `Q` | qualified |
| `D` | dead |

**Detail screen**

| key | effect |
|-----|--------|
| `i` | edit notes — suspends ratatui, runs `$VISUAL` / `$EDITOR` / `vi` on a tmpfile, PATCHes if changed |
| `a` | drop into the Action screen |

**Search**

`/term` then `Enter` saves the query; `n` / `N` jumps forward / backward through matches across `source`, `name`, `email`, `phone`, `source_external_id`. Matches are tinted yellow in the list.

**Cmdline (`:`)**

| command | effect |
|---------|--------|
| `:q` / `:quit` | quit |
| `:r` / `:refresh` | reload leads from the API |
| `:filter source=fb` | narrow view to a source |
| `:filter status=new` | narrow view to a status |
| `:filter tag=hot` | narrow view to a tag |
| `:filter clear` | drop filters |
| `:sort created` / `created-asc` / `next` | reorder view |
| `:reset` | clear filters, sort, search query |
| `:next +3d` / `+2h` / `+30m` / `+1w` / `+45s` | set selected lead's `next_action_at` relative to now |
| `:next 2026-12-31T23:59:00Z` | set `next_action_at` to RFC 3339 absolute |
| `:next clear` | unset `next_action_at` (cancel a booked follow-up) |
| `:tag +hot +warm` | add tags (idempotent) |
| `:tag -cold` | remove tag |
| `:tag clear` | drop all tags |
| `:export /tmp/leads.csv` | dump current view (filter+sort applied) to RFC 4180 CSV |

### Auth

Set `INFRASAAS_API_TOKEN` to require `Authorization: Bearer ` on `/leads/*`. `/healthz` always stays open for load-balancer probes. Comparison is constant-time via `subtle::ConstantTimeEq`. With the variable unset (the dev default), `/leads/*` is open — fine for `127.0.0.1`-bound development, **not safe for public exposure**.

```sh
export INFRASAAS_API_TOKEN=$(openssl rand -hex 32)
curl -H "Authorization: Bearer $INFRASAAS_API_TOKEN" http://127.0.0.1:8080/leads
```

The CRM picks up the same env var and attaches it on every request.

### Rate limiting

Per-IP rate limiting via [`tower_governor`](https://crates.io/crates/tower_governor). Defaults: 5 req/s, burst 20. Override with `INFRASAAS_RATE_PER_SEC` and `INFRASAAS_RATE_BURST`. Excess requests get `429 Too Many Requests`. The bucket key uses `SmartIpKeyExtractor`, which honors `Forwarded` / `X-Forwarded-For` / `X-Real-IP` and falls back to the TCP peer — safe behind a reverse proxy that strips/sets these headers (e.g. relayd) **only as long as the API binds `127.0.0.1`** so untrusted clients can't set them directly.

### Public inbound webhook

`POST /leads/inbound` is a public, no-auth endpoint for ingesting leads from third-party signup forms (landing page, FB DM responder, referral form). Allowed `source` values are configured via the comma-separated `INFRASAAS_INBOUND_SOURCES` env var; if unset, every inbound request is rejected with `400`. Body shape:

```json
{ "source": "landing-page", "email": "x@example.com", "name": "X", "phone": "...", "message": "..." }
```

Server stamps `tags=['inbound']` and `status='new'`. Rate-limiting is the same global governor as authed routes. Server logs only the email *domain*, never the full address — keeps `/var/log/daemon` PII-free.

### OpenBSD hardening

On `target_os = "openbsd"` the API server, after `bind(2)`:

1. `unveil(2)`s a tiny read-only allowlist (`/etc/ssl/cert.pem`, `/etc/resolv.conf`, `/etc/hosts`, `/etc/services`) and then calls `unveil("", "")` to lock further changes. The rest of the filesystem becomes invisible to the process.
2. `pledge("stdio rpath inet dns", None)` drops syscall surface to the bare minimum needed for steady-state operation.

Both calls are no-ops on Linux/macOS so the same binary is portable across dev and prod.

See [`crates/core/src/lead.rs`](crates/core/src/lead.rs) for type definitions.

## Deploy

Production deployment is intentionally minimal: a single static binary, a
local Postgres, and an `rc.d` script. No containers, no orchestrator, no
systemd. Tested against OpenBSD; the same binary runs on Linux with `pledge`
and `unveil` becoming no-ops.

```sh
# 1. apply migrations against a fresh DB
DATABASE_URL=postgres://... ./scripts/migrate.sh

# 2. on OpenBSD: drop the rc.d script in place, install env file + binary
doas install -m 0755 scripts/openbsd/infrasaas /etc/rc.d/infrasaas
doas useradd -d /var/empty -L daemon -s /sbin/nologin _infrasaas
doas mkdir -p /etc/infrasaas
doas install -m 0600 -o _infrasaas .env.example /etc/infrasaas/env
doas $EDITOR /etc/infrasaas/env # fill in secrets
doas install -m 0755 target/release/infrasaas-api /usr/local/bin/
doas rcctl enable infrasaas && doas rcctl start infrasaas
```

`scripts/migrate.sh` is idempotent — it tracks applied files in a
`schema_migrations` table and is safe to re-run on every deploy. The OpenBSD
`rc.d` script sources `/etc/infrasaas/env` at start-time so secrets stay out
of `ps aux` and process-listing tools.

## Roadmap

- **Phase 0** — `core` + `api` + `crm` + `claude_enrich`. (current)
- **Phase 1** — `orchestrator` (cron + queue), email + WhatsApp + scraper integrations.
- **Phase 2** — second consumer onboarded; partner-facing CRM affordances if needed.
- **Phase ∞** — productize as SaaS only if internal usage proves the engine works.

## License

[MIT](LICENSE) © Chris Rodriguez