{"id":49792529,"url":"https://github.com/crodorg/infrasaas","last_synced_at":"2026-05-12T06:06:55.448Z","repository":{"id":355966144,"uuid":"1230457066","full_name":"crodorg/infrasaas","owner":"crodorg","description":"Agnostic outreach + CRM + automation infrastructure. Rust workspace, HTTP API, terminal CRM.","archived":false,"fork":false,"pushed_at":"2026-05-06T03:35:35.000Z","size":131,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-06T04:31:25.697Z","etag":null,"topics":["automation","axum","crm","openbsd","outreach","postgres","ratatui","rust","self-hosted","sqlx","tokio","tui"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/crodorg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T02:38:17.000Z","updated_at":"2026-05-06T03:35:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/crodorg/infrasaas","commit_stats":null,"previous_names":["crodorg/infrasaas"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/crodorg/infrasaas","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crodorg%2Finfrasaas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crodorg%2Finfrasaas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crodorg%2Finfrasaas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crodorg%2Finfrasaas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/crodorg","download_url":"https://codeload.github.com/crodorg/infrasaas/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crodorg%2Finfrasaas/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32926049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-11T17:09:15.040Z","status":"online","status_checked_at":"2026-05-12T02:00:06.338Z","response_time":102,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","axum","crm","openbsd","outreach","postgres","ratatui","rust","self-hosted","sqlx","tokio","tui"],"created_at":"2026-05-12T06:06:51.378Z","updated_at":"2026-05-12T06:06:55.425Z","avatar_url":"https://github.com/crodorg.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# infrasaas\n\n[![CI](https://github.com/crodorg/infrasaas/actions/workflows/ci.yml/badge.svg)](https://github.com/crodorg/infrasaas/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![MSRV](https://img.shields.io/badge/MSRV-1.85-orange.svg)](Cargo.toml)\n[![Rust 2024](https://img.shields.io/badge/edition-2024-dea584.svg)](https://doc.rust-lang.org/edition-guide/rust-2024/)\n\nAgnostic 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.\n\n**Status:** early Phase 0. Scaffolding only. APIs unstable.\n\n## Design\n\n- **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.\n- **Privacy by default.** Self-hosted: own VPS, own Postgres, single static binaries, no telemetry.\n- **Project-agnostic.** A `source` discriminator on the `leads` row routes data per consumer. No domain logic baked into the infra.\n- **Terminal-native CRM.** ratatui TUI binary. No web UI in the critical path.\n- **`unsafe_code = forbid`** at the workspace level.\n- **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.\n- **Secrets are never plain `String`.** API keys and connection URLs are wrapped in `secrecy::SecretString` to block accidental `Debug` / log leaks.\n\n## Workspace\n\n| Crate | Purpose |\n|-------|---------|\n| [`core`](crates/core) | Shared types: `Lead`, `Campaign`, `Integration` trait, errors |\n| [`api`](crates/api) | axum HTTP server. REST endpoints for leads CRUD |\n| [`crm`](crates/crm) | ratatui TUI. 4-screen CRM (list / detail / action / template-picker) |\n| [`integrations/claude_enrich`](crates/integrations/claude_enrich) | Claude API enrichment |\n\nPhase 0 ships the four crates above. Orchestrator, scrapers, email, and WhatsApp integrations are deferred to Phase 1.\n\n## Stack\n\nRust 2024 · tokio · axum · sqlx · ratatui · reqwest · serde · tracing · jiff. Postgres 16+. OpenBSD-friendly single static binaries.\n\n## Quickstart\n\nRequires Rust 1.85+ and a local Postgres 16+.\n\n```sh\ngit clone https://github.com/crodorg/infrasaas.git\ncd infrasaas\ncp .env.example .env\n\n./scripts/dev-up.sh        # boots a user-local Postgres on :5433, applies migrations, starts the API on :8080\n```\n\nIn another terminal:\n\n```sh\ncargo run -q -p infrasaas-crm   # ratatui CRM\n```\n\nInsert a lead:\n\n```sh\ncurl -sX POST http://127.0.0.1:8080/leads \\\n  -H 'content-type: application/json' \\\n  -d '{\"source\":\"example\",\"name\":\"Ada Lovelace\",\"email\":\"ada@example.com\",\"tags\":[\"beta\"]}'\n```\n\nList leads:\n\n```sh\ncurl -s http://127.0.0.1:8080/leads | jq\n```\n\nTear down:\n\n```sh\n./scripts/dev-down.sh\n```\n\n## Build\n\n```sh\ncargo check --workspace\ncargo clippy --workspace --all-targets -- -D warnings\ncargo test --workspace\ncargo build --workspace --release\n```\n\nThe `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.\n\n## API\n\nPhase 0 endpoints (more in subsequent phases):\n\n| Method | Path | Body / Query | Returns |\n|--------|------|--------------|---------|\n| `POST` | `/leads` | `NewLead` JSON | `Lead` |\n| `GET` | `/leads` | `?source=\u0026status=\u0026limit=` | `Vec\u003cLead\u003e` |\n| `GET` | `/leads/{id}` | — | `Lead` |\n| `PATCH` | `/leads/{id}` | `LeadPatch` JSON | `Lead` |\n| `POST` | `/leads/{id}/enrich` | `{prompt_template, max_tokens?, model?}` | `Lead` (with `enriched_data`) |\n| `GET` | `/healthz` | — | `200 OK` (process liveness) |\n| `GET` | `/readyz` | — | `{\"db\":bool,\"enrich\":bool}` (200 ready / 503 not) |\n\n### Enrichment\n\nEnrichment 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`.\n\nDefault 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`).\n\nThe 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.\n\n### CRM keymap\n\nVim-style modal TUI. Three modes: NORMAL (default), CMD (`:`), SEARCH (`/`).\n\n**Movement**\n\n| key | effect |\n|-----|--------|\n| `j` / `k` | next / previous lead in current view |\n| `gg` / `G` | jump to top / bottom |\n| `Ctrl-d` / `Ctrl-u` | half-page down / up |\n| `h` / `Esc` | back one screen |\n| `l` / `Enter` | forward one screen |\n| `q` | quit |\n\n**List screen — quick status flip** (selection stays put, sweep through a batch)\n\n| key | sets status to |\n|-----|----------------|\n| `c` | contacted |\n| `R` | replied |\n| `Q` | qualified |\n| `D` | dead |\n\n**Detail screen**\n\n| key | effect |\n|-----|--------|\n| `i` | edit notes — suspends ratatui, runs `$VISUAL` / `$EDITOR` / `vi` on a tmpfile, PATCHes if changed |\n| `a` | drop into the Action screen |\n\n**Search**\n\n`/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.\n\n**Cmdline (`:`)**\n\n| command | effect |\n|---------|--------|\n| `:q` / `:quit` | quit |\n| `:r` / `:refresh` | reload leads from the API |\n| `:filter source=fb` | narrow view to a source |\n| `:filter status=new` | narrow view to a status |\n| `:filter tag=hot` | narrow view to a tag |\n| `:filter clear` | drop filters |\n| `:sort created` / `created-asc` / `next` | reorder view |\n| `:reset` | clear filters, sort, search query |\n| `:next +3d` / `+2h` / `+30m` / `+1w` / `+45s` | set selected lead's `next_action_at` relative to now |\n| `:next 2026-12-31T23:59:00Z` | set `next_action_at` to RFC 3339 absolute |\n| `:next clear` | unset `next_action_at` (cancel a booked follow-up) |\n| `:tag +hot +warm` | add tags (idempotent) |\n| `:tag -cold` | remove tag |\n| `:tag clear` | drop all tags |\n| `:export /tmp/leads.csv` | dump current view (filter+sort applied) to RFC 4180 CSV |\n\n### Auth\n\nSet `INFRASAAS_API_TOKEN` to require `Authorization: Bearer \u003ctoken\u003e` 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**.\n\n```sh\nexport INFRASAAS_API_TOKEN=$(openssl rand -hex 32)\ncurl -H \"Authorization: Bearer $INFRASAAS_API_TOKEN\" http://127.0.0.1:8080/leads\n```\n\nThe CRM picks up the same env var and attaches it on every request.\n\n### Rate limiting\n\nPer-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.\n\n### Public inbound webhook\n\n`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:\n\n```json\n{ \"source\": \"landing-page\", \"email\": \"x@example.com\", \"name\": \"X\", \"phone\": \"...\", \"message\": \"...\" }\n```\n\nServer 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.\n\n### OpenBSD hardening\n\nOn `target_os = \"openbsd\"` the API server, after `bind(2)`:\n\n1. `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.\n2. `pledge(\"stdio rpath inet dns\", None)` drops syscall surface to the bare minimum needed for steady-state operation.\n\nBoth calls are no-ops on Linux/macOS so the same binary is portable across dev and prod.\n\nSee [`crates/core/src/lead.rs`](crates/core/src/lead.rs) for type definitions.\n\n## Deploy\n\nProduction deployment is intentionally minimal: a single static binary, a\nlocal Postgres, and an `rc.d` script. No containers, no orchestrator, no\nsystemd. Tested against OpenBSD; the same binary runs on Linux with `pledge`\nand `unveil` becoming no-ops.\n\n```sh\n# 1. apply migrations against a fresh DB\nDATABASE_URL=postgres://... ./scripts/migrate.sh\n\n# 2. on OpenBSD: drop the rc.d script in place, install env file + binary\ndoas install -m 0755 scripts/openbsd/infrasaas /etc/rc.d/infrasaas\ndoas useradd -d /var/empty -L daemon -s /sbin/nologin _infrasaas\ndoas mkdir -p /etc/infrasaas\ndoas install -m 0600 -o _infrasaas .env.example /etc/infrasaas/env\ndoas $EDITOR /etc/infrasaas/env                              # fill in secrets\ndoas install -m 0755 target/release/infrasaas-api /usr/local/bin/\ndoas rcctl enable infrasaas \u0026\u0026 doas rcctl start infrasaas\n```\n\n`scripts/migrate.sh` is idempotent — it tracks applied files in a\n`schema_migrations` table and is safe to re-run on every deploy. The OpenBSD\n`rc.d` script sources `/etc/infrasaas/env` at start-time so secrets stay out\nof `ps aux` and process-listing tools.\n\n## Roadmap\n\n- **Phase 0** — `core` + `api` + `crm` + `claude_enrich`. (current)\n- **Phase 1** — `orchestrator` (cron + queue), email + WhatsApp + scraper integrations.\n- **Phase 2** — second consumer onboarded; partner-facing CRM affordances if needed.\n- **Phase ∞** — productize as SaaS only if internal usage proves the engine works.\n\n## License\n\n[MIT](LICENSE) © Chris Rodriguez\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrodorg%2Finfrasaas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrodorg%2Finfrasaas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrodorg%2Finfrasaas/lists"}