Best Practices
CRM — Best Practices
The recommended way to build fast, correct CRM integrations.
✓ Key record bodies by field slug
The data object is keyed by each field's slug (name-8fk2), never the
display name. Fetch slugs once from GET /object/:objectSlug/fields and cache
them. Using the label throws 400 "Field 'X' is required".
✓ Use slugs everywhere, never numeric ids
Objects, records, fields, and relations are all addressed by slug. Slugs are stable across dev/staging/prod and safe to log. Never hard-code or expose an internal id.
✓ Read field metadata to build inputs
List/detail responses include a fields[] block (types + options + virtual
relation fields). Render your form from it instead of hard-coding field names —
that way a new custom field appears automatically.
✓ Send only changed fields on update
PUT accepts a partial data. Send just the fields that changed; the mutation
service emits oldData/newData with only the diff, which keeps audit logs,
workflows, and webhooks precise.
✓ Validate select values against the field's options
select / multi_select / status / priority / tag values must match an
allowed option. Fetch them from the field-options endpoint rather than guessing.
✓ Paginate and filter server-side
GET /object/data/:objectSlug takes page / limit (default 25, capped) plus a
filters array. Push filters and sorting to the server; never fetch a whole
object and filter in the client. Cap filter conditions (max 50 per request).
✓ Treat import/export as async jobs
Large exports/imports return a jobSlug. Poll the status endpoint and show
progress; don't block the UI on the initial request.
✓ Scope API keys to the minimum
Grant a key only the objects and verbs it needs (read.contacts,
create.deals), not a broad *. The key acts as its owner and is still filtered
by that owner's A/G/M/D level per record — but a tight scope is defense in depth.
✓ Prefer webhooks/realtime over polling
Subscribe to record.* webhooks (server → your app) or
the DataEventsGateway (server → UI) for changes. Make webhook handlers
idempotent (dedupe on X-Corteksa-Delivery) and verify the signature.
✗ Don't reach past the pinned connection (backend)
On the shared hyper-tenant DB, all record access must go through the pinned
repoProvider; Bull processors use @TenantScoped(). A raw DataSource query
bypasses RLS and returns zero rows or fails the insert. See
Backend Integration.
✗ Don't re-implement validation or access filtering
Field validation belongs in the FieldTypeRegistry handlers; row-level scope
belongs in access-filter.builder.ts. Adding a new field type means adding a
handler, not a special case in a service.