A CRM that fits your process
Gume
The CRM that follows your sales process, ready for AI agents over MCP, with a public API and a free plan to start.
- commits in 8 weeks
- 621
- ADRs (decision records)
- 48
- test files
- 531
- public API operations
- 170
Figures measured from the repository.
Context and problem
Sales teams spend the day inside a CRM, but almost never inside one that follows their process. A new field becomes a ticket, a new pipeline becomes a project, and automation is a black box: nobody knows what it will do until it does it, in production.
Gume is my product for that problem, a SaaS CRM I am bringing to market at gume.app.br. The pitch is a CRM in Portuguese for teams that want to configure their own process: a kanban pipeline, deal detail and history, custom pipelines and fields, visual automations, a documented API, CSV import, per-person permissions and change history. The UI ships in Portuguese, English and Spanish, and sign-up accepts a Google or Microsoft account. There is a free plan with limits (3 users, 5,000 records, 3 active automations, 500 executions per month and 500 MB). Paid plans are still being defined.
The code is closed and the product is commercial, so this case describes the engineering in prose, without code excerpts. The “Technical details” blocks open with a click, for anyone who wants to see how each part works inside.
Constraints I set myself
- Many customers in the same database, and no read or write path may cross workspaces. Authorization always happens on the server; the UI only reflects what the server decided.
- PostgreSQL is the single source of truth. Redis only holds rate limits and short-lived coordination, and losing Redis must not erase anything.
- Configuration must never require a migration. Creating a field is a metadata write, never an
ALTER TABLE. - Automation must be predictable. Users need to see what a flow would do before publishing it.
- The whole product has to fit on one cheap server, with verified backups and monitoring, until revenue justifies more.
- Soft delete by default, money as
NUMERICand a change history written together with every write.
Decisions that shaped the product
Architecture
Every write goes through the API and stores state, audit and an outbox event in the same PostgreSQL transaction. Only after commit does the worker consume the event and fire automations, webhooks and integrations. AI agents use MCP, which talks to the same REST API and never to the database.If an external provider fails, the event stays in the outbox and the worker replays it with idempotent consumers. A user write never depends on a third party, and losing Redis never destroys CRM data.
In production an edge proxy routes /api/v1/* straight to the API and everything else to Next.js. The worker consumes the outbox, runs automations and handles retention jobs. The MCP server runs on the user’s machine with no database access and no business rules: it turns tools into calls to the same REST API, using a scoped service key.
Technical details: the whole SaaS on a 6 vCPU, 12 GiB VPS7 processes · daily backup
Production is a single Linux host with 6 vCPU and 12 GiB of RAM running seven containerized processes: edge, web, API, worker, PostgreSQL, Redis and MinIO. Only the edge publishes ports (80 and 443). Everything else sits on a private network, and PostgreSQL listens on loopback only, for the backup job.
The memory budget lives in a topology contract that CI checks: 3 GiB for PostgreSQL, 1.5 GiB for the API, 768 MiB for web, 512 MiB each for worker and MinIO, 384 MiB for Redis and 128 MiB for the edge. That leaves about 5.25 GiB for the OS, the container runtime and the page cache. Redis runs with noeviction: if memory runs out it refuses the write instead of silently dropping a rate-limit counter.
Backups run daily: pg_dump, age encryption and upload to a versioned S3 bucket. The identity that produces backups can only write; deleting old copies is a different account with a different credential, and the restore private key lives off the server. Each run writes a manifest with hash, size and the latest applied migration, and only pings the success monitor after verifying the snapshot. If no ping arrives within 26 hours, I get an alert. A full restore rehearsal (dump, encryption, upload, exact-version download, decryption and restore into an empty database) runs in CI.
Deploys go through a script that keeps a dump before every release (retained for 90 days) and has a rollback path. For host health, a small agent reads /proc and Docker every 60 seconds and pushes a snapshot to the API itself, kept for 30 days in PostgreSQL. I picked that over Prometheus and Grafana because a full observability stack would use more memory than the worker.
What the product does inside
Data model and custom fields
Leads, deals, companies, contacts, tasks and products are native objects with specialized columns. On top of them, each workspace creates its own fields with no migration.
Technical details: typed values, composite keys and per-workspace indexesADR-0006 · ADR-0015
A field definition has an immutable UUID, an owner type (lead, deal, company or contact), a machine key, a value type and single or multiple cardinality. Values are ordered rows in a shared table. Each row repeats the definition’s owner type, value type and cardinality under a composite foreign key, and a check requires that only the matching typed column is filled. The database rejects text in a number field even if the application gets it wrong.
Type and cardinality never change after creation. An incompatible change becomes a new field plus an explicit data migration, so old rows are never reinterpreted. Deleting a definition is logical and keeps its values. References to another record or to a workspace member are polymorphic, so a registered adapter per entity locks and validates the target in the same transaction; a reference into another workspace fails.
Exact and range filters use shared B-tree indexes led by workspace, definition and typed value, with no dynamic per-customer indexes. Fuzzy search is not promised until a measured query plan justifies its cost. There are hard limits per field, per value and per request, because multi-value fields multiply rows and history size.
Tenant isolation follows the same idea: tables carry workspace_id, and composite foreign keys include the workspace, so those relationships cannot point at another customer’s data even through an application bug. On top of that, services and repositories check the workspace again after route authorization.
Automations and simulation
The visual canvas builds flows with a trigger, conditions, CRM actions, waits and HTTP calls. Before publishing, the user runs a simulation that shows what each step would do.
Technical details: versions, durable waits and simulation on shadow statedraft/published · fixtures
Each automation has a draft and published versions, and an execution is pinned to the version that was published when the trigger fired. Editing the flow does not change what is already running.
A wait is persisted state. The execution moves to WAITING with a resume time, and the cadence scheduler wakes due ones with FOR UPDATE SKIP LOCKED, so work is shared without two instances grabbing the same row. A business-day wait pins the calendar version at scheduling time. On resume, the cancel condition is evaluated against the current record, so a deal that has already been won does not get the follow-up that was scheduled.
Simulation receives only workspace read ports, an in-memory shadow state that simulated actions update and an HTTP fixture resolver. An HTTP step only “succeeds” if the request carries the exact fixture for that node; without it, the simulation returns a denial before any network adapter is even on the path. Waits become virtual time. The output is a versioned trace with the record before and after, and nothing is stored: two identical simulations give the same logical result and leave no trace in the database.
Plans show up here too. Executions count against a monthly counter per workspace; above the cap the execution is not created, the trigger is marked as blocked by the plan and never retried, and a single audit event per month records that the cap was hit.
Events, outbox and worker
Outbound webhooks, automations and integrations leave through the outbox, never from the user’s request.
Technical details: idempotent claims, leases and signed webhooksoutbox · at-least-once
The worker reads committed events and handles each in one transaction that does two things together: it inserts the event claim (ignoring conflicts) and writes the effect. If the process dies halfway, there is no claim without an effect and no effect without a claim. Automation executions use a lease with a token and an expiry; a worker that lost its lease cannot write a step result. The polling loop comes straight back when the last pass found work, so a backlog drains without waiting for the idle interval.
An outbound webhook freezes the canonical event body, signs each attempt with HMAC, carries a stable identity so the receiver can deduplicate, and has retry with backoff, a dead-letter queue and manual replay by an admin. The network call happens in its own consumer, outside the internal outbox consumer, which only touches PostgreSQL. An inbound webhook verifies the endpoint signature, stores an idempotent receipt and the outbox event, and only then answers 202; the execution is created later, asynchronously.
The same worker runs the personal-data retention job every hour, in batches of 1,000 rows and on database time: webhook and email bodies, raw import rows and automation step outputs are nulled or redacted on windows from 7 days to 12 months, and old trails are deleted.
Access: sign-in, permissions and API keys
People sign in with email and password, Google or Microsoft. The admin decides what each member can see and change, and integrations use scoped keys.
Technical details: server-side sessions, fail-closed profiles and Redis admissionArgon2id · HMAC · 5 buckets
A session is an opaque token in an HttpOnly cookie; the database stores only a keyed digest, and every request resolves user and membership in PostgreSQL. Passwords use Argon2id. Browser mutations require an exact Origin and a CSRF token. Google accounts are identified by verified sub and Microsoft accounts by tenant plus sub; a Microsoft email collision fails instead of linking accounts. A single hub handles sign-in and environment selection, and moving between hosts uses a one-time handoff stored in Redis; the target host creates its own PostgreSQL session.
The roles are admin and member. Members can get a named profile with separate read, create, update and delete capabilities for leads, deals, companies and contacts (16 combinations). An empty, disabled or deleted profile grants nothing, never the default. Capabilities are resolved in the same transaction as the request, with no cache, so a revoked grant takes effect on the next call.
API keys are opaque, shown once, have a mandatory expiry and are verified by HMAC with a pepper keyring that supports rotation. Only an admin signed in with a cookie can create, rotate or revoke keys; a key cannot manage another key. A request carrying both a cookie and a Bearer token is rejected. Every endpoint must be classified in the route registry, with a human capability and a service scope, or the API refuses to start; the OpenAPI document is generated from that registry.
Per-key rate limiting is a fixed window in Redis. One atomic script checks and increments five counters at once (key version, principal, workspace, route and traffic class) or increments none, and nothing is refunded once admitted. If Redis fails, only key traffic gets a 503; people in the browser keep reading.
API and MCP server
The API is the same surface the app and the agents use, documented in OpenAPI 3 and published with an interactive reference, llms.txt and a “copy for AI” export. The public spec has 170 operations across 136 routes. A few choices that make integrations hard to break:
- Moving a deal to another stage has its own endpoint (
POST .../stage-transitions). The regular update does not accept a stage, and the stage history is immutable. - Updates accept
expected_updated_at. If someone edited first, the write fails instead of silently overwriting. - Opaque cursor pagination and stable error codes, so clients branch on the code and never on text.
- Routes that only make sense for the app session stay out of the public docs on purpose.
Technical details: 58 MCP tools, each bound to one REST operationstdio · untrusted client
The MCP server is a Node.js 24 process using the official SDK over stdio. It authenticates with a workspace service key and behaves as an untrusted REST client: it does not import the database package, opens no PostgreSQL connection and holds no business rules. Workspace and acting identity always come from authentication, never from what the agent sent.
There are 58 tools in six groups: workspace inspection, pipelines and stages, leads, deals, custom fields (sections, definitions and options) and automations with their executions. Each tool points to one canonical REST operation and declares the scope it needs and whether it reads or mutates data. There is no generic HTTP, SQL or code-execution tool.
Running and simulating an automation are separate tools, and simulation is classified as a read. An agent with a read-only key can test flows freely with no real effect. Mutations are never retried blindly, stdout is reserved for the protocol and diagnostics go to stderr already stripped of credentials, prompts and response bodies. Language interpretation, planning and operator approval stay with the agent.
My role
Everything: product vision, architecture, code, tests, deployment and operations. All 621 commits in the repository, from August 20 to October 18, 2026, are mine. Development is driven by specs and ADRs and accelerated by AI agents: every relevant decision is recorded before it becomes code, which kept 48 decisions consistent over 8 weeks. There are 96 SQL migrations and 10 CI workflows, most of them on my own runners.
Outcome
- Native leads, deals, companies, contacts, tasks and products, custom fields, multiple pipelines with open, won and lost stages, kanban, table views and saved dashboards.
- Persistent visual automation with a canvas, safe simulation, execution traces, durable waits and business-day cadences.
- REST API with OpenAPI, scoped service keys, inbound and outbound webhooks and an MCP server for AI agents.
- SaaS operations: per-workspace plans and limits, encrypted and verified backups, personal-data retention enforced by the worker, granular access profiles and sign-in with Google and Microsoft.
What I would do differently
- Generate the contract between canvas and kernel instead of keeping hand-synced copies between the web app and the engine package.
- Keep ADR numbering disciplined from day one. Speed produced duplicate numbers; it is minor, but it is exactly the kind of noise a document-driven process should not have.







