CRM:一款开源、以智能体为核心的客户关系管理系统
CRM: An open-source, agentic-first CRM

原始链接: https://github.com/trycompai/crm

该项目是一个开源的、“代理优先”(agentic-first)的客户关系管理系统(CRM),从根本上颠覆了传统的软件模式。它并非在数据库上外挂一个人工智能聊天机器人,而是一个将 CRM 作为其笔记账本进行维护的自主智能体。 **主要功能包括:** * **持久的自主性:** 基于 Vercel 的 “eve” 框架构建,智能体按自身计划运行。它能够管理自己的工作队列,决定研究内容,并执行后续跟进,无需人类提示或请求-响应循环。 * **基于证据的记录:** 为了避免人工智能“一本正经地胡说八道”,该智能体不会进行猜测。它记录观察结果(如签名块或 GitHub 身份),并将它们视为证据进行评估。不可靠的数据仅作为建议留待人工审核。 * **隐私优先架构:** 该系统采用单租户和仅限内部使用的设计,使用基于 Google 的身份验证。智能体在安全的、网络受限的沙箱中运行,以防止数据泄露。 * **技术栈:** 使用 Bun、Next.js、NestJS 和 Prisma 构建的 Turborepo 单体仓库。即便在没有 API 密钥的情况下,它也能通过将内部电子邮件和会议数据作为最可靠的事实来源,从而有效地发挥作用。 简而言之,这是一个自我管理的助理,它将 CRM 条目转化为可查证的审计追踪记录。

Hacker News 最新 | 往期 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 CRM:一个开源的、以智能体为先的 CRM 系统 (github.com/trycompai) 8 分,由 handfuloflight 发布于 1 小时前 | 隐藏 | 往期 | 收藏 | 讨论 | 帮助 考虑申请 YC 2026 年秋季班!申请截止日期为 7 月 27 日。 准则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

An open-source, agentic-first CRM.
A durable research agent is the product. The database is just where it writes things down.

The agent · Stack · Quick start · Configuration · Deploying · Contributing

MIT licence Built with eve Built with Bun Postgres

The CRM overview: closed-won and open pipeline, pipeline by stage, deals in progress and overdue tasks


Most CRMs are a database with a form in front of it. The AI ones bolt a chat box onto the side of that form. Both leave the actual work — finding out what is true, and writing it down — to a human who has better things to do.

This is built the other way round. The agent is not a feature of the CRM; the CRM is where the agent keeps its notes. It runs on its own deployment, on its own schedule, against its own work queue. It decides what to look at next, books its own follow-ups, spends a research budget, and stops when the budget runs out. Nothing about it is request-response: close the browser and it keeps going.

The API deliberately has no intelligence in it at all. NestJS reports that something happened — a thread was ingested, a company was created, an attendee is unknown — by writing a row to a queue. The agent leases that row and decides what it means. A Nest service that calls an enrichment API is treated as a bug, and docs/api.md explains the outage that made that a rule.

The rule the agent itself never breaks: nothing about a person is guessed. No tool accepts a confidence score, because a model asked to grade its own certainty will, and it will be wrong in the direction that makes it look useful. Tools report what they observedcrm.signature-block, github.account-identity — and a ledger prices the evidence. Strong evidence writes to the record. Weak evidence becomes a suggestion a human settles. A confidently wrong fact about a customer is worse than a blank field, because nobody can tell it is wrong.

It is single-tenant and internal by design. Sign-in is Google, the allow-list is one environment variable, and everyone who gets in can see everything. That is the whole authorisation model — see SECURITY.md before you point it at real customer data.

Deals list with stage, amount, owner and close date

Deals — filters, sort and page live in the URL, so a view is a link.

Contacts list with title, email, company and owner

Contacts — most of these were created by the mailbox sync, not typed.

Companies list with domain, industry, open pipeline and owner

Companies — logo, industry and location arrive on their own.

Overview dashboard

Overview — yours or the whole team's, toggled in the URL.

apps/agent is its own deployment, built on eve — Vercel's filesystem-first framework for durable agents. A tool is a file, a skill is a markdown file, a schedule is a file, and the runtime handles the durable part: sessions that survive a redeploy, work that resumes where it stopped.

18 authored tools read_crm_history, search_crm, identify_contact, research_person, enrich_company, record_fact, schedule_recheck
4 skills evidence.md, identity-matching.md, data-boundaries.md, writing-a-brief.md — prose the agent reads, versioned like code
1 schedule dispatch.ts, which decides nothing. It leases what is due and starts a session per row.
A sandbox bash, grep, glob and a /workspace, with deny-all egress

It runs itself. lib/tasks.ts is the work queue: claimDue leases rows with FOR UPDATE SKIP LOCKED, so two dispatchers take disjoint work and a run that dies frees its row when the lease expires. Anything that looks like "every N minutes, the oldest ten contacts" belongs in a task's dueAt, not in a cron expression. When the agent wants another look at somebody it calls schedule_recheck and says why — and the reason is shown to the rep, because an agent that cannot say why it will be back in fourteen days does not have a reason, it has a default.

Every outside source is optional, and it is designed to run with none of them. With no API keys at all it still works: read_crm_history reads your own threads, meetings and signature blocks, which is free and is the best evidence there is — no data vendor can sell you a reply from the person's own address. Each key opens one more place to look. It is told at the start of every session which ones this install has, so it plans around what it actually has rather than discovering the gaps one failed call at a time, and it prints the list at startup:

[agent] on   LinkedIn (RAPIDAPI_KEY)
[agent] off  Web research (PERPLEXITY_API_KEY)
[agent] off  Company brand data (CONTEXT_DEV_API_KEY)

The sandbox has no network and no database. Turning it on is what gives the model a shell — the difference between a tool-caller and something that can keep a dossier, diff this month's profile against last month's, and grep a thread for a signature block. deny-all egress costs nothing, because web_fetch runs in the app runtime and web_search at the model provider. What it removes is the only path by which a customer's email body could leave through a shell command. The other half of that rule is an absence: the sandbox is never given DATABASE_URL. A shell with credentials and egress is exfiltration-shaped even in an internal tool; a shell with neither is a text processor.

You can talk to it, and watch it work. Every contact, company and deal has an Agent tab — the steps as it takes them, the leads it throws away and why, and its questions answered in place when it cannot decide between two people. Conversations are durable and survive a reload; the record travels in a signed token rather than being bolted onto the front of your message. Set AGENT_BRIDGE_SECRET to the same value in both processes to turn it on. Without it the tab reports that it is not configured, and the agent carries on running its own schedule.

docs/agent.md is the full write-up.

A Turborepo monorepo on Bun, deployed on Vercel.

Agent eve — durable sessions, tools, skills, schedules, sandboxes
Model Vercel AI Gateway — no provider SDK, and OIDC on Vercel means no key to manage
Sandbox Vercel Sandbox in production, Docker or microsandbox locally
Front end Next.js App Router · shadcn/ui · nuqs for URL state
API NestJS with nestjs-trpc — HTTP, auth, tRPC, Google sync
Data Prisma · Postgres (Neon) · optional Redis (Upstash)
Auth Better Auth, Google-only, one allow-list
Files Vercel Blob — mirrors profile pictures so they survive the source going away
Tooling Biome · TypeScript everywhere

The app talks to the API over tRPC, and the router type is generated from the NestJS routers — so the front end is type-safe from the Prisma row to the table cell. List state (filters, sort, page) lives in the URL, so copying the address bar reproduces the view.

Path
apps/agent The research agent — tools, skills, schedules, sandbox
apps/app Next.js front end · :3000
apps/api NestJS API — HTTP, auth, tRPC, Google sync · :3001
packages/db Prisma schema, migrations, shared Postgres client
packages/auth Better Auth config and the sign-in allow-list
packages/ui shadcn/ui components, the Tailwind theme
packages/env Finds and loads the root .env

Three rules the codebase holds to

Written up where the work happens, not in a style guide:

  • Intelligence never lives in the API (docs/api.md). Nest reports that something happened; the agent decides what it means. Two copies of an identity matcher once drifted until one matched every employer on earth.
  • packages/ui is the only source of UI (docs/design.md). No overriding styles at the call site.
  • There are no organizations. Single tenant, deliberately. An organizationId that is always the same value is a column, an index and a permissions check that buys nothing and reads like a real one at review time.

You need Bun and Docker.

git clone https://github.com/trycompai/crm.git && cd crm
bun install

docker compose up -d          # Postgres on :5432
cp .env.example .env          # then fill in the four values below

bun run db:deploy             # apply migrations
bun run db:seed               # optional: a believable pipeline to look at
bun run dev

The app is on localhost:3000, the API on localhost:3001.

Open .env and set these. Everything else in the file is optional and commented out.

Variable What to put in it
BETTER_AUTH_SECRET openssl rand -base64 32
ALLOWED_SIGN_IN Your email domain, e.g. acme.com. Or one address, e.g. [email protected].
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET A Google OAuth client — 2 minutes, below.

DATABASE_URL already matches the docker compose Postgres, so leave it alone unless you brought your own.

Getting the Google OAuth client
  1. Google Cloud consoleCredentialsCreate credentialsOAuth client IDWeb application.
  2. Under Authorised redirect URIs, add http://localhost:3001/api/auth/callback/google.
  3. Enable the Gmail API and the Calendar API for the project.
  4. Copy the client ID and secret into .env.

Google sign-in is the only way in, so the API will not start without these. If your account is on a Google Workspace domain, set the consent screen to Internal and nobody outside your org can even reach the prompt.

ALLOWED_SIGN_IN is the entire authorisation model — an unset value means nobody can sign in, which is the safe direction to fail. It takes whole domains, individual addresses, or a mix:

ALLOWED_SIGN_IN="acme.com"                       # everyone at your company
ALLOWED_SIGN_IN="acme.com,[email protected]"  # …plus one outsider
ALLOWED_SIGN_IN="[email protected]"                  # a one-person install

There is one .env, at the root of the repo, read by all three processes. Real environment variables always win, so on a hosting platform you configure it there and the file is purely a local convenience.

Beyond the four required values, everything is optional and the app runs without any of it. .env.example is the full list with a note on each; the short version:

API_URL / APP_URL Where the two halves are served. Only needed off localhost.
PERPLEXITY_API_KEY Lets the agent search the open web, with citations.
RAPIDAPI_KEY Lets the agent read LinkedIn profiles for identity.
CONTEXT_DEV_API_KEY Company logo, industry and socials from a domain.
AGENT_BRIDGE_SECRET Lets a rep talk to the agent from a contact's Agent tab.
REDIS_URL A shared cache. Without it, per-instance and in-memory.
CRON_SECRET Guards the Gmail/Calendar sync route. Required to use it.
Command
bun run dev Everything, in watch mode
bun run build Build all apps and packages
bun run test Run the test suite
bun run check-types tsc --noEmit everywhere
bun run lint / format Biome
bun run db:migrate Create and apply a migration
bun run db:seed Top up the demo pipeline (idempotent)
bun run db:studio Prisma Studio
bun run --filter=api trpc:generate Regenerate the AppRouter type
bun run --filter=api dev:session Print a session cookie for a local user

Scope any of them with a Turborepo filter: bun run dev --filter=api.

Because Google is the only door, there is no way to get a session from a terminal — dev:session writes the rows Better Auth would have written and prints the cookie it would have set. It refuses to run with NODE_ENV=production.

Three deployments and a Postgres: the Next.js app, the NestJS API, and the agent. They are independent, and the only thing they must agree on is DATABASE_URL and BETTER_AUTH_SECRET — the API mints the session cookie and the app verifies it, so a mismatch is a redirect loop rather than an error.

Set API_URL and APP_URL to the real origins, and if the two are on different subdomains of one parent, set AUTH_COOKIE_DOMAIN to the parent so one cookie covers both. Add http://your-api-host/api/auth/callback/google to the OAuth client's redirect URIs. Set CRON_SECRET and point a scheduler at POST /internal/sync/google to keep the mailbox sync running.

apps/api/src/generated/server.ts is committed and build must never regenerate it — the generator needs a newer GLIBC than most build images have. Regenerate locally and commit it with the router change that caused it.

We'd rather have a paragraph you wrote than a pull request an agent wrote. See CONTRIBUTING.md.

Security issues go through SECURITY.md, privately, not a public issue.

MIT.

联系我们 contact @ memedata.com