Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Use Founder / Developer tabs in the docs sidebar to filter this page.
| Stage | What happens |
|---|---|
| Configure | Channels in ring-config.json; passwords in CRM_CHANNEL_* secrets |
| Ingest | Cron polls each enabled mailbox, or a signed webhook pushes payloads |
| Process | Security scan → AI classification → CRM upsert with sourceChannel → draft queue |
| Review | Admins filter inbox by channel, approve drafts |
| Send | Replies leave via CRM SMTP for that channel (not login noreply) |
| Track | Tasks and API cost roll up in admin analytics |
Idempotency: duplicate Message-ID values are skipped; overlapping poll batches are mutex-guarded.
001_email_crm_schema.sql defines email_analytics, email_knowledge_base, and email_security_events. No TypeScript module reads or writes these tables. Use 009 + 010 JSONB migrations instead.
Next-step: operator checklist, channel secrets, and admin CRM shell.
Deep-dive: channels status route and sourceChannel query contract.
Depends-on: Auth SMTP plane stays on lib/mailer.ts.
See-also: how DB_BACKEND_MODE selects the db() adapter.
Use Founder / Developer tabs in the docs sidebar to filter this page.
| Stage | What happens |
|---|---|
| Configure | Channels in ring-config.json; passwords in CRM_CHANNEL_* secrets |
| Ingest | Cron polls each enabled mailbox, or a signed webhook pushes payloads |
| Process | Security scan → AI classification → CRM upsert with sourceChannel → draft queue |
| Review | Admins filter inbox by channel, approve drafts |
| Send | Replies leave via CRM SMTP for that channel (not login noreply) |
| Track | Tasks and API cost roll up in admin analytics |
Idempotency: duplicate Message-ID values are skipped; overlapping poll batches are mutex-guarded.
001_email_crm_schema.sql defines email_analytics, email_knowledge_base, and email_security_events. No TypeScript module reads or writes these tables. Use 009 + 010 JSONB migrations instead.
Next-step: operator checklist, channel secrets, and admin CRM shell.
Deep-dive: channels status route and sourceChannel query contract.
Depends-on: Auth SMTP plane stays on lib/mailer.ts.
See-also: how DB_BACKEND_MODE selects the db() adapter.
Use Founder / Developer tabs in the docs sidebar to filter this page.
| Stage | What happens |
|---|---|
| Configure | Channels in ring-config.json; passwords in CRM_CHANNEL_* secrets |
| Ingest | Cron polls each enabled mailbox, or a signed webhook pushes payloads |
| Process | Security scan → AI classification → CRM upsert with sourceChannel → draft queue |
| Review | Admins filter inbox by channel, approve drafts |
| Send | Replies leave via CRM SMTP for that channel (not login noreply) |
| Track | Tasks and API cost roll up in admin analytics |
Idempotency: duplicate Message-ID values are skipped; overlapping poll batches are mutex-guarded.
001_email_crm_schema.sql defines email_analytics, email_knowledge_base, and email_security_events. No TypeScript module reads or writes these tables. Use 009 + 010 JSONB migrations instead.
Next-step: operator checklist, channel secrets, and admin CRM shell.
Deep-dive: channels status route and sourceChannel query contract.
Depends-on: Auth SMTP plane stays on lib/mailer.ts.
See-also: how DB_BACKEND_MODE selects the db() adapter.
features/email-crm/types/* | Pure types (no runtime deps) — breaks Jsonb circular require |
features/email-crm/repositories/ | import type from types; JSONB repos |
features/email-crm/lib/jsonb-collection.ts | readDoc / upsertDoc / queryDocs → db() |
app/api/cron/email-processor/route.ts | Constructs processor per action |
app/api/admin/email/channels/route.ts | Read-only channel status |
db()CRM repositories and services never call raw SQL. Types are split into features/email-crm/types/{contact,task,draft}.ts so repositories can import type without pulling service factories into a CJS require cycle.
| Helper | db() method |
|---|---|
readDoc(collection, id) | readDoc() |
upsertDoc(collection, id, record) | readDoc() then updateDoc() or createDoc() |
queryDocs({ collection, filters, orderBy, limit }) | queryDocs() |
deleteDoc(collection, id) | deleteDoc() |
| Collection | Document id | Indexed fields |
|---|---|---|
email_threads | RFC thread root / Message-ID | status, fromEmail, lastMessageAt, sourceChannel |
email_contacts | contact_<sha256(email)> | email, type |
email_messages | RFC Message-ID | threadId |
email_drafts | draft_<uuid> | threadId, status |
email_tasks | task_<uuid> | threadId, status, dueDate |
email_api_usage | req_<timestamp>_<random> | timestamp, operation, emailId |
Migrations: 009_email_crm_jsonb.sql + 010_email_crm_tasks_jsonb.sql.
EmailMessageService.exists(messageId) at start of handleEmail.pollInboundBatch returns { skipped: true }.pollBatch waits for all handleEmail promises before disconnect.uid === 0 skips IMAP markAsSeen.| Mode | When | Mechanism |
|---|---|---|
| poll (recommended) | k8s CronJob, serverless | Connect → fetch UNSEEN → process → disconnect |
| start | Dedicated Node pod | Persistent IMAP IDLE via instrumentation.ts |
| HTTP start | Debug only | EMAIL_PROCESSOR_ALLOW_HTTP_START=true + cron action:start |
Bearer $CRON_SECRET or HMAC X-Email-Webhook-Signature.isPlatformAdmin.Same-workflow: orders desk under CrmAdminShell.
features/email-crm/types/* | Pure types (no runtime deps) — breaks Jsonb circular require |
features/email-crm/repositories/ | import type from types; JSONB repos |
features/email-crm/lib/jsonb-collection.ts | readDoc / upsertDoc / queryDocs → db() |
app/api/cron/email-processor/route.ts | Constructs processor per action |
app/api/admin/email/channels/route.ts | Read-only channel status |
db()CRM repositories and services never call raw SQL. Types are split into features/email-crm/types/{contact,task,draft}.ts so repositories can import type without pulling service factories into a CJS require cycle.
| Helper | db() method |
|---|---|
readDoc(collection, id) | readDoc() |
upsertDoc(collection, id, record) | readDoc() then updateDoc() or createDoc() |
queryDocs({ collection, filters, orderBy, limit }) | queryDocs() |
deleteDoc(collection, id) | deleteDoc() |
| Collection | Document id | Indexed fields |
|---|---|---|
email_threads | RFC thread root / Message-ID | status, fromEmail, lastMessageAt, sourceChannel |
email_contacts | contact_<sha256(email)> | email, type |
email_messages | RFC Message-ID | threadId |
email_drafts | draft_<uuid> | threadId, status |
email_tasks | task_<uuid> | threadId, status, dueDate |
email_api_usage | req_<timestamp>_<random> | timestamp, operation, emailId |
Migrations: 009_email_crm_jsonb.sql + 010_email_crm_tasks_jsonb.sql.
EmailMessageService.exists(messageId) at start of handleEmail.pollInboundBatch returns { skipped: true }.pollBatch waits for all handleEmail promises before disconnect.uid === 0 skips IMAP markAsSeen.| Mode | When | Mechanism |
|---|---|---|
| poll (recommended) | k8s CronJob, serverless | Connect → fetch UNSEEN → process → disconnect |
| start | Dedicated Node pod | Persistent IMAP IDLE via instrumentation.ts |
| HTTP start | Debug only | EMAIL_PROCESSOR_ALLOW_HTTP_START=true + cron action:start |
Bearer $CRON_SECRET or HMAC X-Email-Webhook-Signature.isPlatformAdmin.Same-workflow: orders desk under CrmAdminShell.
features/email-crm/types/* | Pure types (no runtime deps) — breaks Jsonb circular require |
features/email-crm/repositories/ | import type from types; JSONB repos |
features/email-crm/lib/jsonb-collection.ts | readDoc / upsertDoc / queryDocs → db() |
app/api/cron/email-processor/route.ts | Constructs processor per action |
app/api/admin/email/channels/route.ts | Read-only channel status |
db()CRM repositories and services never call raw SQL. Types are split into features/email-crm/types/{contact,task,draft}.ts so repositories can import type without pulling service factories into a CJS require cycle.
| Helper | db() method |
|---|---|
readDoc(collection, id) | readDoc() |
upsertDoc(collection, id, record) | readDoc() then updateDoc() or createDoc() |
queryDocs({ collection, filters, orderBy, limit }) | queryDocs() |
deleteDoc(collection, id) | deleteDoc() |
| Collection | Document id | Indexed fields |
|---|---|---|
email_threads | RFC thread root / Message-ID | status, fromEmail, lastMessageAt, sourceChannel |
email_contacts | contact_<sha256(email)> | email, type |
email_messages | RFC Message-ID | threadId |
email_drafts | draft_<uuid> | threadId, status |
email_tasks | task_<uuid> | threadId, status, dueDate |
email_api_usage | req_<timestamp>_<random> | timestamp, operation, emailId |
Migrations: 009_email_crm_jsonb.sql + 010_email_crm_tasks_jsonb.sql.
EmailMessageService.exists(messageId) at start of handleEmail.pollInboundBatch returns { skipped: true }.pollBatch waits for all handleEmail promises before disconnect.uid === 0 skips IMAP markAsSeen.| Mode | When | Mechanism |
|---|---|---|
| poll (recommended) | k8s CronJob, serverless | Connect → fetch UNSEEN → process → disconnect |
| start | Dedicated Node pod | Persistent IMAP IDLE via instrumentation.ts |
| HTTP start | Debug only | EMAIL_PROCESSOR_ALLOW_HTTP_START=true + cron action:start |
Bearer $CRON_SECRET or HMAC X-Email-Webhook-Signature.isPlatformAdmin.Same-workflow: orders desk under CrmAdminShell.