CRM scaling
CRM scaling and launch checklist
A CRM stays fast by paginating every list, indexing the main questions, and deferring heavy work until a measured trigger fires. At launch, apply 10 rules, watch 8 signals and hold the performance targets below.
Last checked
Rules to apply at launch
- Pagination: Every table view, record timeline, kanban column and search uses keyset pagination over an indexed sort key plus id, page size capped at 100; the query builder refuses sorts and filters on non-indexed columns for large entities and never emits OFFSET.
- Search: Filters are a validated AST compiled by one parameterised query builder (allow-listed fields and operators, fields the viewer cannot read are rejected), with statement_timeout (STATEMENT_TIMEOUT_MS) and a row cap on every request; free-text uses tsvector GIN and trigram GIN.
- Search: Custom fields live in a jsonb column with a GIN jsonb_path_ops index for containment filters; range sorts on a custom field are allowed only after the field is promoted to a typed column by migration. EAV tables are rejected because filtering needs a join per field.
- Idempotency: Jobs are chunked (500 rows) and idempotent: imports upsert on email_norm/domain, bulk ops record (bulk_op_id, chunk_no), exports are rebuilt not resumed, and API creates accept an Idempotency-Key header, so a retry or double click never duplicates or double-applies.
- Idempotency: Records carry a version integer; updates send the expected version and a mismatch returns 409 with the current record, so two reps editing the same deal cannot silently overwrite each other.
- Caching: Dashboards and reports read report_daily rollups and a per-workspace cached KPI payload (revalidated on write via tag) instead of aggregating deals or activities per request; the kanban fetches 50 cards per stage and hydrates owners with one batched query.
- Rate limits: Per-user and per-workspace limits on search, record writes, imports, exports and bulk ops (for example 1 concurrent import, 3 exports per hour, 5 bulk ops per hour per user); over-limit requests get 429 with Retry-After and are never queued silently.
- Data growth: audit_log and deal_stage_history are append-only and partitioned (monthly and yearly); activities partition by year past 50M rows; notifications and export/import metadata have retention deletes; reports never scan raw history.
- Storage: Import sources and export files live in private buckets with lifecycle expiry (sources 7 days, exports 24 h); attachments on records are capped per file and per workspace.
Performance targets
| What to measure | Target |
|---|---|
| Table view page (100 rows, filtered, sorted) | p95 < 300 ms, <= 5 queries, index-backed at 1M contacts |
| Record page (fields, timeline first 25, tasks) | p95 < 250 ms; LCP < 2.5 s |
| Kanban board load (6 stages x 50 cards) | p95 < 400 ms; card move p95 < 150 ms |
| Global search | p95 < 300 ms, <= 50 results, keyset paginated |
| Dashboard / report load | p95 < 500 ms from rollups, no raw table scans |
| Import throughput | >= 1,000 rows per second validated, >= 500 rows per second committed per worker; 50k rows under 3 minutes |
| Export of 50k rows | ready in < 60 s; no impact on interactive p95 |
What to watch
- Metric query latency ms by view: histogram for list/search queries by entity and filter shape; slow-query log above 500 ms.
- Metric import export volume: rows imported and exported per user per day; feeds the exfiltration alert.
- Metric conflict 409 rate: optimistic concurrency conflicts per entity; a rise means a UX or race problem.
- Log structured audit-adjacent request logs: request id, hashed actor id, entity, action, row count; no field values, no PII, no tokens.
- Trace import upload -> validate -> commit trace: one trace id across the three stages with row counts.
- Slo crm availability: 99.9% of record and list requests succeed under 1 s; error budget reviewed monthly.
- Alert audit chain break: any hash-chain verification failure pages immediately.
- Alert bulk or import stuck: job running > 15 min without chunk progress, or failure rate > 5% over 30 min.
Recurring work
- validate_import: runs queue (import_jobs uploaded). Guard: streams the file, max IMPORT_MAX_MB and IMPORT_MAX_ROWS, writes at most 1,000 error rows, commits nothing, attempt cap 3.
- commit_import: runs queue (user confirmed). Guard: chunks of 500 upserted on email_norm or domain, chunk results idempotent, stops and reports on the first systemic error, per-workspace concurrency of 1 import.
- build_export: runs queue (export_jobs). Guard: keyset walk, row count checked against EXPORT_MAX_ROWS before writing, field permissions applied, formula-injection neutralised, file expires in EXPORT_URL_TTL_SECONDS.
- apply_bulk_op: runs queue (bulk_ops). Guard: matched_rows <= BULK_MAX_ROWS, chunks of 500 keyed by (bulk_op_id, chunk_no), per-record permission re-check and audit entry, version check skips conflicted records and reports them.
- rollup_report_daily: runs cron 15m. Guard: recomputes only days touched since last run, upsert by (day, owner, pipeline, stage), bounded batch.
- task_reminders: runs cron 1m. Guard: claims due reminders with SKIP LOCKED, dedupes per (user, task), REMINDER_EMAILS_PER_USER_DAY cap.
Add when the numbers say so
- Read replicas: Route report, search and export reads to a read replica when primary CPU or read latency crosses the trigger; the data layer separates read and write paths so this is configuration, and exports already tolerate replica lag.
- Fanout: Outbound webhooks and automation are driven from a transactional outbox with per-endpoint queues, signed payloads, backoff and circuit breakers so one slow subscriber cannot stall others.
- Search: Move global search to a dedicated search engine once tsvector/trigram latency or index size crosses the trigger; the query builder already isolates search behind one interface.
Deferred until it is needed
| Deferred | Adopt it when |
|---|---|
| Outbound webhooks and a public REST API with scoped keys | first integration request or > 20% of customers ask. |
| Email and calendar sync (Gmail / Microsoft 365 OAuth) | email logging is the top requested feature. |
| Workflow automation engine and duplicate-merge tooling | customers hand-run the same steps > 20 times a week. |
| Dedicated search engine (OpenSearch/Typesense) | > 10M records per workspace or search p95 > 300 ms. |
| Read replicas for reports, search and exports | primary CPU > 60% sustained or report p95 > 500 ms. |
| Real-time co-editing and live presence | frequent simultaneous edits on the same record. |
On Forge64
Forge64 matches your description to one of 12 reference architectures, writes the plan and data model into the project, generates the app, and runs a quality gate over the code before you see it. Each app gets its own Postgres database and sign-in with no per-user fee. Hosting on Forge Cloud and Forge Payments are rolling out.
Every metered resource on Forge64 has a hard cap, so a traffic spike stops at your limit instead of becoming a surprise bill. This is a reference design for the category, not a promise that every item is generated today. Built-ins such as email, background jobs, file uploads and search are coming soon, so anything here that needs them is something you add yourself for now.
FAQ
- When should I worry about scale for a CRM?
- Not before a measured trigger fires. For example, for "Outbound webhooks and a public REST API with scoped keys": first integration request or > 20% of customers ask.
- What performance targets should a CRM aim for?
- Table view page (100 rows, filtered, sorted): p95 < 300 ms, <= 5 queries, index-backed at 1M contacts. Record page (fields, timeline first 25, tasks): p95 < 250 ms; LCP < 2.5 s. Kanban board load (6 stages x 50 cards): p95 < 400 ms; card move p95 < 150 ms.
- Does Forge64 cap my usage costs?
- Yes. Every metered resource has a hard cap, and paid plans add an overage spend cap that you set.
Build your app with Forge64
One prompt to a running app with auth, database, hosting and payments. Start free, no credit card required.
Start building for free