Skip to content
Sendora Cloud
Create account
Changelog

Shipped.

What's new in Sendora Cloud, week by week.

Subscribe via RSS or in your email (footer).

2026-06-01SDK
Support unread count — SDK helpers across all 4 SDKs (Wave 73)
  • `support.getUnreadCount(widgetId)` on iOS / Android / RN. New `useUnreadTicketCount(widgetId)` hook on `@sendoracloud/sdk-react`. Web React hook auto-polls (60s while visible, paused on hidden, immediate re-fetch on return).
  • All four return 0 on no-auth / network / parse error — host's badge logic never crashes.
  • `@sendoracloud/sdk-react-native@1.5.0` + `@sendoracloud/sdk-react@0.3.0` LIVE on npm.
2026-06-01Platform
Support unread count + per-row hint (Wave 72)
  • Migration 0069 — `support_tickets.last_seen_by_customer_at` + partial unread index. Bumped on every authed `GET /widgets/:id/my-tickets/:ticketId`.
  • New `GET /widgets/:widgetId/my-tickets/unread-count` Bearer-JWT endpoint returns `{ count: number }`. 5s threshold defeats the instant +1 right after a ticket is created by the submitter.
  • Ticket list endpoint now returns `unread: boolean` per row. Worker `/embed/tickets` shows a blue dot on unread rows.
2026-06-01SDK
Web ContactWidget + TicketHistory in @sendoracloud/sdk-react 0.2.0 (Wave 71)
  • `<ContactWidget />` + `useContactWidget`, `<TicketHistory />` + `useTicketHistory` — drop-in iframe components for React apps. Single-HTML-source pattern over the same worker embed routes the mobile shells use.
  • Worker `?embed=iframe` switches close-channel from `sendora://` (native WebView intercept; blocked in browsers) to `window.parent.postMessage`. Wrapper validates `MessageEvent.origin === workerHost` before dispatching close.
  • Auto-resolve identity from `SendoraProvider`'s `AuthUser`. Esc + click-outside dismiss. Iframe sandbox locked to `allow-forms allow-scripts allow-same-origin allow-popups`.
2026-06-01Platform
Push notification on agent reply (Wave 70)
  • Every agent reply on a ticket carrying a stitched `user_id` (W68) fires a push notification to that user's registered tokens. Closes the awareness loop opened by W69's in-app ticket history.
  • Category `transactional` bypasses the marketing-consent gate. Per-token suppression + quiet-hours still enforced.
  • Payload `data.{ kind: 'support_reply', ticketId, replyId }` so mobile deep-link handler routes the tap straight into `presentTicketHistory`.
2026-06-01Platform
In-app ticket history + reply across iOS / Android / RN (Wave 69)
  • `presentTicketHistory(widgetId)` on iOS / Android + `<TicketHistory />` + `useTicketHistory` on React Native. Submitters can read their thread + reply inline.
  • Three Bearer-JWT endpoints under `/widgets/:widgetId/my-tickets`. Filter scoped to `(user_id = jwt.sub) OR (email = jwt.email AND user_id IS NULL)` — anon rows + identified users + legacy pre-signin tickets all surface.
  • JWT in URL hash fragment (not query) + scrubbed on load via `history.replaceState` — never appears in server logs / referer.
  • Customer reply auto-reopens resolved/closed tickets + emits `support.in_app_reply` event.
2026-06-01Platform
Contact widget identity handling — anon + identified user (Wave 68)
  • Native shells now auto-stitch tickets to the SDK's signed-in user. Identified users see email field locked + `userId` attached. Anonymous SDK rows get `userId` only. No SDK auth → standard form.
  • Worker embed gains `prefillUserId` + `lockEmail=1` query params. Locked-email mode hides input + renders read-only chip with the verified value.
  • Backend `widgetSubmitSchema.subject` is now optional — derived from first-line/80-chars of message when absent, enabling the single-textarea mobile UX.
2026-06-01Platform
Dashboard surface for Slack Connect (Wave 67)
  • New `/dashboard/support/slack-connect` page closes the W64 carry-over — customers configure the bot end-to-end without curl.
  • One-time Slack App setup walkthrough with the customer's Events Subscription URL surfaced inline.
  • Bound-state card shows last-4 of bot token + signing secret (full secrets never echoed) + lastPostAt + lastError. Edit / Disconnect controls.
2026-06-01SDK
Mobile Contact Widget — iOS / Android / RN parity with the web widget (Wave 66)
  • New worker route `https://go.sendoracloud.com/embed/contact?widgetId=…` ships ~6 KB self-contained HTML contact form. Posts to `/api/v1/widgets/:id/submit` (same backend as the web `widget.js`). Bot-check + rate-limit run server-side.
  • iOS — `SendoraCloud.support?.presentContactWidget(widgetId:, from:, theme:, prefillName:, prefillEmail:, completion:)`. WKWebView in a UIViewController .formSheet. `sendora://close*` intercept dismisses + fires `SendoraContactResult { ticketId, portalUrl, submitted }`.
  • Android — `SendoraCloud.support.presentContactWidget(activity, widgetId, …, onResult)`. WebView in `SendoraContactWidgetActivity` (programmatic, no XML). Same close-URL contract.
  • React Native — new subpath import `@sendoracloud/sdk-react-native/contact-widget` ships `<ContactWidget />` + `useContactWidget()`. `react-native-webview` is an **optional** peer dep — Metro only resolves it when the subpath is used.
  • Updating contact UI = redeploy worker once. Zero SDK release. Same shell will host future surfaces: `/embed/chat`, `/embed/kb`, `/embed/portal/:token`, `/embed/auth`.
  • `@sendoracloud/sdk-react-native@1.2.0` LIVE on npm. iOS + Android tag `sdk-{ios,android}-v…` carry-over for SwiftPM + JitPack publishing.
2026-06-01SDK
@sendoracloud/sdk-react 0.1.0 — drop-in <SignIn /> + <UserButton /> (Wave 65)
  • New `@sendoracloud/sdk-react` package ships drop-in React components for end-user auth: `<SignIn />`, `<SignUp />`, `<ForgotPassword />`, `<UserButton />`, plus `<SignedIn>` / `<SignedOut>` gates and `<SendoraProvider>` root. Clerk parity for the prebuilt-component story; closes the `❌ — bring your own UI` on `/vs/clerk`.
  • Mount mirrors Clerk: `<SignIn />` renders email/password + optional social provider buttons + MFA challenge handoff (auto-switches to 6-digit field when SDK returns `mfaRequired`) + optional magic-link button.
  • Zero CSS dep — class names are namespaced `sdr-` and styles auto-inject into `<head>` on first mount. No bundler config. SSR-safe (skips when `document` is undefined).
  • Peer deps: `react ^18 || ^19` + `@sendoracloud/sdk-web >=3.0.0 <4.0.0`. ESM 14.2 KB / CJS 15.6 KB minified.
  • Operator carry-over: publish `@sendoracloud/sdk-react@0.1.0` to npm.
2026-06-01Platform
Support — Slack Connect channel (Wave 64)
  • Per-(orgId, projectId) Slack channel binding posts every new ticket + agent reply into the bound Slack channel via `chat.postMessage`. Thread replies in Slack route back to the ticket as public replies via the Slack Events API webhook.
  • Bot token + signing secret stored AES-256-GCM at rest. Inbound Events API requests verified against the per-integration signing secret (v0 HMAC-SHA256 over `v0:${ts}:${rawBody}` with a 5-min replay window).
  • Migration `0068_support_slack_integrations.sql` adds `support_slack_integrations` table + `support_tickets.slack_message_ts` + `support_ticket_replies.slack_user_id` / `slack_message_ts`. Hard-delete sweep includes the new table.
  • Plan-gated Business+ via the existing entitlements matrix (`slackConnect` feature). New `support.reply_added_via_slack` event registered in the canonical webhook taxonomy.
  • Flips the Zendesk comparison `Slack Connect channel` row from ❌ → ✅. Closes Wave 14 retract.
2026-05-31SDK
Android SDK — Play Install Referrer helper (v4.1.0)
  • sdk-android 4.1.0 ships an opt-in helper that consults Google Play `InstallReferrerClient` on first launch and routes the install through the new `POST /attribution/install-referrer` endpoint. Server-side deterministic match on `sendora_link_id` / `gclid` / `fbclid` / `ttclid` / `utm_source+utm_campaign` runs BEFORE fingerprint/IP fallback — surviving Play Store handoff lossiness.
  • Dep is `compileOnly` (`com.android.installreferrer:installreferrer:2.2`) — host app opts in by adding it. Class-lookup guard at runtime; apps without the dep skip the path cleanly and fall back to `/attribution/install`.
  • Closes the W47 backend-only carry-over.
  • Operator carry-over: tag `v4.1.0` on `github.com/sendoracloud/sdk-android` for JitPack.
2026-05-31Platform
Analytics — multi-chart canvas
  • A canvas pins N saved insights at coordinates on a 12-column grid — compose once, load in one click. Closes the Mixpanel/Amplitude boards parity gap end-to-end.
  • Backend: migration 0065 adds `platform.analytics_canvases` (jsonb layout array, cap 50 entries by schema). Service `runCanvas` executes pinned insights in parallel via the existing per-kind dispatcher; deleted insights collapse to `result=null + error` so the grid stays stable.
  • Routes: `GET/POST /canvases`, `GET/PATCH/DELETE /canvases/:id`, `GET /canvases/:id/run`. Literal `/run` registered before `/:id`.
  • Dashboard: `/dashboard/analytics/canvases` list + `[canvasId]` grid renderer with edit mode (pin/unpin from saved-insights library, resize 1/4 → 1/3 → 1/2 → 2/3 → full). New "Canvases" tab beside Boards.
  • Schema validation in shared: `canvasLayoutEntrySchema` bounds (uuid insightId + x/y/w/h grid units).
2026-05-31Platform
Chatbot — drop-in chat widget bundle (chat.js)
  • One-line embed: `<script src="https://go.sendoracloud.com/chat.js" data-chatbot=UUID async>` adds a floating chat button + AI-replied panel to any page. ~8.5 KB, dependency-free, works in any browser from the last 5 years.
  • New public endpoint `POST /widgets/:widgetId/chat` (widget UUID is the key — no API key needed on a public page). Server mints `sessionId` on first call; widget echoes back for multi-turn context.
  • Same bot-check layering as `/widgets/:widgetId/submit`: Origin/Referer + UA + honeypot + signed challenge token. Per-IP rate limit 30/min.
  • Reuses `replyWithFallback` (RAG over Knowledge Base via gpt-oss:20b on Ollama Cloud + canned fallback when AI off). Emits `chatbot.message_sent` with `source: "widget"`.
  • Distinct from `/widget.js` (Contact Widget → ticket via email form). Use Chat Widget for AI deflection, Contact Widget for email handoff.
  • Closes the long-standing Intercom-Messenger comparison gap.
2026-05-31Platform
Analytics — retention heat-grid drill-down
  • `GET /orgs/:orgId/analytics/retention/drill-down` returns the user list behind any (cohortDate, period) heat-grid cell. Period 0 = cohort members; period N>0 = subset still active in that bucket.
  • Reuses the `user_first` CTE from `getRetention`, narrows to one cohort bucket via `date_trunc`, groups `user_periods` on the requested period_num. Sibling COUNT(DISTINCT uid) for pagination.
  • Profiles join surfaces email/name where externalId is on file; cross-project drift returns first match (same posture as profile timeline).
  • `audienceId` cohort scoping forwards from /retention.
  • Dashboard: heat-grid cells clickable → modal w/ paginated user list + Prev/Next paging. Cohort-size button opens at period=0. 0% cells non-clickable.
2026-05-31Platform
Attribution — Play Install Referrer ingest (backend)
  • New public endpoint `POST /attribution/install-referrer` accepts Google InstallReferrerClient payload (URL-encoded referrer + Play-reported install_begin/referrer_click timestamps + google_play_instant flag).
  • Migration 0064 adds `install_events.referrer / referrer_parsed (jsonb) / install_begin_at / referrer_click_at / google_play_instant` + expression index on `referrer_parsed->>'sendora_link_id'`.
  • `ingestInstallReferrer` priority match: `sendora_link_id` (0.95) → `gclid` (0.95) → `fbclid` (0.95) → `ttclid` (0.95) → `utm_source + utm_campaign` (0.65) → fallback to fingerprint/IP `resolveAttribution`.
  • Survives Play Store handoff where deep-link + fingerprint matching alone is lossy. Closes the 100% mobile-attribution match path triplet — click_id at edge (W42) + referrer at install (W47) + S2S to ad-networks (W37-39).
  • Same orgId/projectId auto-derive + anti-spoof posture as `/attribution/install`. Same 24h device_id dedup so re-fired InstallReferrerClient doesn't double-bill.
2026-05-31Platform
Messaging — single-call audience send across Email + Push + SMS
  • `POST /orgs/:orgId/email/send-to-audience` resolves audience → member profiles with non-null email → fan-out one `sendEmail` per recipient. Each per-recipient call enforces consent/suppression/dedup/probation unchanged.
  • `POST /orgs/:orgId/push/send-to-audience` joins audience profiles → active `push_tokens` via `externalId === userId` → fans out via existing `dispatchTokens` pipeline. Per-token consent gate / frequency cap / quiet hours / Critical Alerts / Web Push / APNs / FCM dispatch all apply.
  • `POST /orgs/:orgId/sms/send-to-audience` reads `traits.phone` from member profiles → fans out queued `smsSends` rows with consent gate / TCPA suppression / 60s recent-duplicate dedup / fire-and-forget `dispatchSms`.
  • Shared `batch_id` + `audience_id` stamped into every per-recipient row's metadata for operator correlation. Per-recipient errors caught + logged so one bad recipient doesn't abort the batch.
  • Quota reserved upfront against `estimateAudienceSize`. Cap 5000 recipients per call (predictable memory + quota math); beyond that → background-job version (post-roadmap).
  • Sequential fan-out (not Promise.all) — keeps per-org burst limiter sane on a 5K send.
2026-05-30SDK
Deep Links — `prewarm()` gains 5-concurrent-mint cap on all 4 SDKs
  • Web / RN / iOS / Android — `Links.prewarm()` adds a 5-concurrent-inflight token bucket on top of the existing 5-min TTL + 50-entry LRU. Calls that would push past the cap silently no-op so a runaway loop (eg `prewarm()` inside a FlatList renderItem or a SwiftUI List body or a Compose LazyColumn item) can't fan out unbounded mints + burn through plan quota.
  • No behavioural change for well-behaved callers. Overflow `prewarm()` is fine to skip because prewarm is fire-and-forget by contract; the next matching `create()` will perform the mint inline.
  • Operator carry-over: publish sdk-web + sdk-react-native to npm; tag iOS + Android against `sendoracloud/sdk-ios` + `sendoracloud/sdk-android` for SwiftPM + JitPack.
2026-05-30Platform
Support — audience-routed SLA policies
  • Migration 0058: `sla_policies.audience_id` nullable column + partial index `(org_id, priority, audience_id) WHERE audience_id IS NOT NULL AND is_active = true`.
  • New matcher `resolveSlaPolicy()` in `support/routes.ts`: scans active SLA policies for the ticket's priority sorted `audience_id desc, created_at desc`, resolves reporter profile by `(orgId, projectId, externalId=userId | email)`, calls `isProfileInAudience` for each audience candidate, first match wins, falls back to global policy.
  • Backward-compatible: existing rows + callers keep working with `audience_id` null. Zero overhead for orgs not authoring audience-scoped policies (profile lookup is skipped entirely when no audience candidate exists for the priority).
  • Closes Wave 14 Support honest-retract gap on "audience-routed SLAs".
2026-05-30Platform
SMS — STOP keyword now suppresses email + push too
  • User intent on SMS STOP is "stop marketing to me" not "stop on this channel only". `propagateStopAcrossChannels` (sms/suppression.ts) now scans most-recent `consent_records` row for the phone, extracts any co-recorded email + userId, writes a fresh revocation row covering all 3 identifiers (`granted=false purpose=marketing`).
  • Belt-and-suspenders: email surfaced from consent row is also added directly to `email_suppressions` so orgs with `enforce_consent=false` still stop emailing the recipient.
  • STOP event payload gains `propagatedEmail` + `propagatedUserId` so dashboard + webhook subscribers see what got suppressed.
  • START reverse-propagation deliberately stubbed: re-enabling email after an SMS START is a privacy-sensitive call that should require explicit re-consent via the form, not a keyword reply.
  • Closes Wave 17 SMS honest-retract gap on "cross-channel STOP".
2026-05-30Platform
SMS — `sms.replied` event with `inReplyToSendId` binding
  • Twilio inbound webhook previously emitted variable-string `sms.received` (uncaught by the taxonomy-drift gate which only matches string literals). Split into three explicit canonical event types — STOP family → `sms.stop_received`, START family → `sms.resubscribed` (new), anything else → `sms.replied` (new).
  • For `sms.replied`, best-effort lookup of the most recent outbound to the same number in the last 24h (`sms_sends.status IN ('sent','delivered')`) populates `inReplyToSendId`. Workflows can now trigger on "replied to campaign X" instead of just "got any message". Null when no recent outbound found.
  • Closes Wave 17 SMS honest-retract gap on "`sms.replied` first-class event".
2026-05-30Platform
Knowledge Base — public helpful-vote endpoint
  • `POST /public/kb/:orgSlug/:articleSlug/helpful` — atomic `helpful_count + 1` UPDATE (no read-modify-write race) + fire-and-forget `kb.article_helpful` event mirror-write. Per-IP rate-limited at 10/min. No body, no decrement, no unvote — intent is positive signal only.
  • Returns `{ helpfulCount }` so the marketing renderer can update the count inline without re-fetching the article.
  • Closes Wave 18 KB honest-retract gap on "helpful-vote endpoint".
2026-05-30Platform
Events — `experiment.assigned` + `kb.article_viewed` mirror-written to canonical bus
  • Feature flags — `recordEvaluation` now mirror-writes `experiment.assigned` event with `{ flagId, flagKey, value, ruleMatched }` properties + `{ userId, entityId }` context alongside the `flag_evaluations` insert. Workflows can now trigger on variant assignment; Analytics can join evaluations to downstream conversions.
  • Knowledge Base — public article-detail endpoint mirror-writes `kb.article_viewed` event with `{ articleId, articleSlug, title, categoryId }` properties + `{ user_agent, referer }` context next to the view-counter bump. Feeds Analytics top-articles, Audiences ("people who viewed billing article"), Automation triggers on KB engagement.
  • Both writes stay fire-and-forget so a counter blip can't tank the parent render. Two new categories in the canonical webhook taxonomy: `experiments` + `kb`.
  • Closes Wave 18 KB + Wave 21 Experiments honest-retract gaps on the missing event surface.
2026-05-30Platform
Consent — push + SMS gates now enforced
  • Marketing-class push + SMS sends now honour the same consent ledger as email broadcasts. `sendPushSchema` + `sendPushTopicSchema` + `sendSmsSchema` gain optional `category` field (`transactional | broadcast | workflow | auth`, default `broadcast`).
  • Push — per-token gate inside `dispatchTokens` looks up `{userId: token.userId}` consent when `enforce_consent=true` + category is broadcast/workflow. Anonymous tokens (`token.userId === null`) suppress under enforcement — strict explicit-opt-in posture. Gate-fail row carries `status='suppressed' suppressedReason='no_consent'` + `push.suppressed` event.
  • SMS — `{phone: input.to}` lookup before the suppression-list check. Gate-fail writes `status='failed' provider='suppressed' metadata.suppressed_reason='no_consent'` + new `sms.suppressed` event.
  • Drift parity test (`developer-tools/consent-gate-drift.test.ts`) asserts all 3 channels import `shouldSend` + `isConsentEnforced` from the canonical consent service + write `no_consent` on gate fail.
  • Closes the Wave 1a follow-up specifically called out in the Privacy product page + Wave 15 Email retract.
2026-05-30Docs
Experiments — multivariate / statistical readout / winner auto-promote / `experiment.assigned` event retracted
  • Audit of `feature-flags/service.ts` + `featureFlags` schema + `createFlagSchema` surfaced 4 over-claims. (1) "Flag types — boolean, string, JSON, multivariate" — Zod enum is `boolean / string / number / json`; no `multivariate` value. (2) "Statistical experiment readout via Analytics — funnels + retention auto-cohorted by variant" — no statistical engine; `flag_evaluations` table records variant assignments but auto-cohorting doesn't exist. (3) "Winner auto-promote to In-App Messages + Push A/B variants — same statistical engine" — fabrication. (4) "Workflow trigger — `experiment.assigned` becomes a first-class event" — `recordEvaluation` writes to `flag_evaluations` table only; nothing is written to the `events` table.
  • Real shape: CRUD + toggle endpoints, evaluate + evaluate-all, rule engine (max 20 rules per flag, each `{ audienceId?, percentage?, value? }`), hash-bucket per-entityId sticky rollouts, per-environment toggles, audit log on toggle, evaluation log in `flag_evaluations` (fire-and-forget), SDK helpers on Web + RN.
  • LaunchDarkly comparison rewritten honest. 4 new rows specifically mark statistical readout (❌), winner auto-promote (❌), scheduled flag changes (❌), `experiment.assigned` first-class event (❌). whyStay LEADS with the missing features. Pricing column adds LaunchDarkly Foundation's $12/seat + $10/1K MAU.
  • Real wins kept + emphasised: audience-targeted rollouts in the same tenant as Customers, sticky-by-user hash bucketing, per-environment toggles, kill-switch + audit log, evaluation log for BYO experiment readout via SQL join on `entityId`.
2026-05-30Docs
Attribution — multi-touch / fraud filters / S2S postbacks / Play Install Referrer / cohort LTV retracted
  • Audit of `attribution/routes.ts` + `attribution/service.ts` surfaced 6 over-claims. (1) "Multi-touch attribution — first-touch / last-touch / linear / position-based models" — no model code; single-touch only. (2) "Cohort LTV + ROAS by source / medium / campaign — joined against real Analytics events" — stats endpoints return install counts; no LTV / ROAS computation. (3) "Fraud filters — click flooding / install hijacking / CTIT outliers" — no fraud filter code. (4) "S2S postbacks to ad networks — Meta / Google / TikTok conversion APIs" — no postback code. (5) "Android Play Install Referrer (100% accurate)" — not integrated. (6) "Audience join — attributed installs auto-tag with source campaign as a trait" — no auto-tag wiring.
  • Real shape: install ingest (`POST /attribution/install`), deferred ingest (`POST /attribution/deferred`), iOS canonical fingerprint match (IP-pinned + 2h window) via Deep Links service, stats endpoints by-campaign + by-source returning install counts, per-org config CRUD, SDK helpers `reportInstall()` + `checkDeferred()` on Web + RN.
  • AppsFlyer comparison rewritten honest. 7 new rows specifically mark multi-touch models (❌), Play Install Referrer (❌), Protect360 fraud filters (❌), S2S postbacks (❌), cohort LTV computation (❌), single-touch (✅). New whyStay LEADS with all 6 missing features. Pricing column adds AppsFlyer's published $0.07/conversion (~$700/mo at 10K installs). 3 FAQs rewritten openly admitting the gaps; new FAQ added for Android deferred-install accuracy.
  • Real wins kept + emphasised: iOS canonical-fingerprint deferred match, same-tenant join surface (build your own LTV roll-up in Analytics module against the same `user_id`), SDK on Web + RN.
2026-05-30Docs
Deep Links — prewarm / Play Install Referrer / branded custom domain retracted
  • Audit of `links/service.ts` + `links/routes.ts` + Android SDK surfaced 3 over-claims. (1) `links.prewarm()` — no prewarm code anywhere in backend or SDK; every create is a real HTTPS round-trip. (2) "Android Play Install Referrer (100% accurate)" — no Play Install Referrer integration in Android SDK; iOS canonical-fingerprint match is the only deferred-install path today. (3) "Custom branded domains with wildcard SSL" — no custom-domain code in repo; all short links served from one canonical Sendora short domain.
  • Real shape: SDK-side `links.create()` bundle-id-gated, typed `LinkError(code, statusCode)` codes, custom JSON `linkData<T>` payload on warm + cold open, iOS deferred-install via IP-pinned fingerprint match (2h window, atomic flip), auto-served AASA + assetlinks at `/internal/aasa` + `/internal/assetlinks`, per-link OG override (`ogTitle` + `ogImageUrl`), SDK-side `revoke()` + `getStats()`, real-time click analytics with geo + device + OS + referrer.
  • Branch comparison rewritten honest. 3 new rows mark branded custom domains (Sendora ❌), Play Install Referrer deferred match (Sendora ❌), SDK prewarm cache (Sendora ❌). whyStay LEADS with the missing features. Pricing column adds Branch Activation's ~$199/mo published tier.
  • Real wins kept + emphasised: iOS canonical-fingerprint deferred match, auto-AASA/assetlinks, typed errors, SDK revoke + stats, per-link OG override.
2026-05-30Docs
Knowledge Base — custom domain / ticket auto-link / KB-view event / helpful-vote endpoint retracted
  • Audit of `knowledge-base/routes.ts` + `kb_articles` schema + public KB surface surfaced 5 over-claims. (1) "Custom-domain ready" public help center — no custom-domain code anywhere. (2) "Ticket deflection — incoming tickets auto-link matching articles" — not built; chatbot RAG does this for the widget submission path only, not for email-arriving tickets. (3) "Operators see which articles a user read before opening a ticket" — no ticket-side KB-reads timeline. (4) Helpful-votes — `helpful_count` column exists on `kb_articles` but there's NO `POST /kb/articles/:id/helpful` endpoint. (5) "KB opens become events in Analytics → audiences → workflows" — view bump is a fire-and-forget column update; no `kb.article_viewed` event is written to the platform `events` table.
  • Real shape: articles CRUD with categories + tags + slug + status (draft/published), public SSR'd page at `/help/<org-slug>` with 60s revalidate, view counters auto-incremented on public-fetch, AI chatbot RAG via Postgres ILIKE + top-5 → top-3 → gpt-oss:20b, in-app `kb.search()` SDK on Web 2.16+ + RN 0.18.2+.
  • GitBook comparison rewritten honest. 4 new rows specifically mark versioning (❌), custom domain (❌), automatic ticket deflection (❌), article-view event on analytics bus (❌), helpful-vote endpoint (❌). whyStay LEADS with the missing features. Pricing column adds GitBook's published $65/site + $12/user.
  • Real wins kept + emphasised: 3 surfaces from one article (public SSR page / AI chatbot RAG / in-app `kb.search()`) — not the 4 the old copy claimed.
2026-05-30Docs
SMS — MessageBird/Sinch / audience send / link auto-wrap / cross-channel STOP / replied event / region caps retracted
  • Audit of `sms/routes.ts` + `sms/suppression.ts` surfaced 6 over-claims on the SMS module — largest single-module retract in the sweep. (1) "BYOP — Twilio, MessageBird, Sinch" — only Twilio in code; `provider: 'twilio'` hardcoded on send. (2) Audience-targeted single-call send — takes one `to: string`. (3) Short link auto-wrap via Deep Links — no wrap code anywhere. (4) STOP opt-outs suppress every Sendora channel — `sms_suppressions` is SMS-only + per-org. (5) `sms.replied` first-class event — not emitted; inbound non-keyword messages aren't surfaced. (6) Per-region cost controls (international spend caps) — not built.
  • Real shape: Twilio BYOP (`POST /orgs/:orgId/sms/send` + signed webhook URL `POST /sms/webhooks/twilio/:orgId`), CTIA-compliant STOP keyword set (STOP/STOPALL/UNSUBSCRIBE/CANCEL/END/QUIT + START/UNSTOP/YES re-subscribe) writing to `sms_suppressions`, pre-send suppression check + recipient-dedup 60s window, per-org quota meter via `reserveQuota`, `sms.sent / .delivered / .failed` events, audit log per send.
  • Twilio comparison rewritten honest. 7 new rows enumerate the gaps (MessageBird/Sinch adapter, audience send, link auto-wrap, two-way `sms.replied`, region caps, Verify integration, cross-channel STOP). whyStay LEADS with all 6 missing features. Pricing column adds Twilio's ~$0.0083/segment US.
2026-05-30Docs
Push — audience send / attribution auto-wrap / A-B stats + auto-promote retracted
  • Audit of `push/service.ts` + `push/policies.ts` + `push/templates.ts` surfaced 4 over-claims on the Push module — heaviest claim density of any module audited. (1) "Audience-targeted sends — pick a saved audience from Customers, send" — `sendPush` takes `userIds[]` or `tokenIds[]`; no audienceId expansion. (2) "Attribution-aware — every push gets a deep link via Links; opens + downstream conversions tie back to send for ROAS" — no automatic Deep-Link auto-wrap on push payloads. (3) "Visual A/B testing — winner declaration + statistical readout" — `setPushAbTestStatus(winnerKey)` is a manual operation; no statistical readout engine. (4) "Silent push" — no `silent`/`content-available` convenience param exposed; you'd build via raw payload.
  • Real shape: APNs (cert + token) + FCM + Web Push (VAPID, lazy keypair per-org), Live Activities cross-platform via `pushLiveActivities.platform`, Geofences with enter/exit/dwell, Critical Alerts (iOS DND bypass), per-org frequency caps + TZ-aware quiet hours (`evaluateQuietHours`), sticky-by-user A/B variant assignment (`assignAbVariant`), templates + localization (locale exact match → 2-letter fallback → default), rich payloads + action buttons (max 4 APNs limit), open/click tracking, invalid-token pruning + 30-day GC cron, device-takeover on signin.
  • OneSignal comparison rewritten honest. 4 new rows enumerate the gaps: single-call audience send (Sendora ❌), automatic deep-link wrap (Sendora ❌), A/B statistical readout + auto-promote (Sendora ❌), critical alerts (Sendora ✅). whyStay LEADS with the missing features. Pricing column adds OneSignal Growth's ~$139/mo at 10K mobile MAU.
  • Real wins kept + emphasised: device-takeover on signin (kills duplicate-push class of bug), cross-platform Live Activities, TZ-aware quiet hours, sticky A/B variant assignment, 4-token-source invalid-token pruning, 30-day GC.
2026-05-30Docs
Email — Liquid / platform-wide suppression / audience-targeted send retracted; Wave 1a consent gate surfaced
  • Audit of `email/service.ts` + `email/suppression.ts` + `email_suppressions` schema surfaced 3 over-claims. (1) "Template library with Liquid interpolation" — actual `renderTemplate` is regex `{{var}}` replace; no Liquid `{% if %}`, no helpers. (2) "Suppression list enforced platform-wide" — `email_suppressions` table is `(org_id, email)` keyed; an unsubscribe on org A does NOT propagate to org B. (3) "Audience-targeted sends — pick an audience from Customers, send" — `sendEmail` takes one `to: string` recipient; audience fan-out happens via Automation workflow, not a single-call audience send.
  • Wave 1a consent gate (shipped earlier this session) was missing from the product page. Now surfaced as a top-line feature: opt-in `enforce_consent` toggle on org settings; when on, broadcast + workflow categories check the most-recent `marketing` consent for `input.to` before dispatch. No record OR revoked → `email_sends` row with `status='failed' provider='suppressed' suppressed_reason='no_consent'`.
  • Resend comparison rewritten honest. New rows for React Email templates (Sendora ❌), single-call audience broadcast (Sendora ❌), recipient dedup (Sendora ✅ 60s window — Resend ❌), consent enforcement at send time (Sendora ✅ Wave 1a — Resend manual). whyStay LEADS with React Email + Resend Audiences Broadcast as the two real misses.
  • Real wins kept + emphasised: BYOD via real Resend Domains integration, 4-mode category routing (Cloudflare for platform-critical / Resend for customer mail), content scanner, probation pool, recipient-dedup, RFC 8058 unsubscribe, 4-source bounce/complaint webhook fan-in (Svix + SNS + CF synchronous + CF GraphQL poller).
2026-05-30Docs
Support — Slack Connect / audience-routed SLAs / assignment rules / round-robin retracted
  • Audit of `support/routes.ts` + `inbound.ts` + `sla_policies` schema surfaced 4 over-claims. (1) Slack Connect channel — not built, repo has zero matches. (2) Audience-routed SLAs ("enterprise + active 6mo+ gets 1h SLA") — `sla_policies` schema is priority-keyed (firstResponseMinutes + resolutionMinutes + businessHoursOnly per priority); no `audienceId` column. (3) Assignment rules + round-robin — not built, manual assignee per ticket. (4) "CSAT flows back to profile + triggers detractor / promoter Workflows" — already retracted in Wave 9.
  • Real shape: tickets CRUD + replies + public portal at `/ticket/:token` + inbound email pipeline with 4 routing modes (reply-token / plus-addressed / BYOD / slug subdomain) + priority-based SLA policies + canned responses + saved views + CSAT survey + Contact Widget → ticket pipeline.
  • Zendesk comparison rewritten honest. 4 new rows explicitly mark Slack Connect, audience-routed SLAs, assignment rules + round-robin, automatic CSAT trait write as ❌ for Sendora. whyStay LEADS with these missing features. 2 FAQs rewritten (Macros / Triggers / SLA routing) acknowledging the gaps and offering workarounds. New FAQ added for round-robin saying it's on the roadmap with no committed date.
  • Real wins kept + emphasised: 4-mode inbound email pipeline (the differentiator), reply-token threading on every Sendora zone, same-tenant userId cross-link to Customers + Auth + Audiences, Contact Widget RAG over KB.
2026-05-30Docs
Analytics — Boards / heat-grid / auto-enroll cohort retracted; honest funnel-export wording
  • Audit of `analytics/routes.ts` + `analytics/service.ts` surfaced 4 over-claims. (1) "Saved Insights + Boards — Boards mix funnels + retention + time-series on one canvas" — no Boards code in backend or dashboard. Saved Insights exist; Boards do not. (2) "Retention cohort heat-grid drill-down + auto-enroll drop-off segments into Workflows" — retention endpoint returns curves only, no heat-grid; no auto-enroll wiring anywhere. (3) "Funnel + path analysis with per-step bars + drop-off table + audience export for every step" — `computeFunnel` returns step counts, no audience-export-per-step. (4) "Activation funnel tuning where drop-off cohorts auto-enroll in save-attempt drips" — same auto-enroll fabrication.
  • Real shape: 20+ analytics endpoints covering overview / time-series / multi-time-series / top events / breakdown / breakdown-by-trait / paths / retention curves / realtime / funnels (save + compute) / Saved Insights + auto-discovered property + trait dropdowns / AI features (conversational debug + lifecycle classifier + auto-trait extraction + anomaly detection).
  • Mixpanel comparison rewritten honest. New rows enumerate Boards (❌), retention heat-grid (❌), auto-enroll funnel drop-off (❌). whyStay LEADS with the missing features. Pricing column adds Mixpanel's published $0.28/1K events at 10K MAU.
  • Product page now openly says: "Build the audience export manually via the Customers module + bind via `audienceId`." Customers know they're getting most of Mixpanel's surfaces, not all of them.
2026-05-30Docs
Customers (CDP) — probabilistic stitching / computed traits / ad-network sync retracted
  • Audit of `profiles/auto-upsert.ts` + `profiles/routes.ts` + `audiences` schema surfaced 3 over-claims. (1) "Deterministic + probabilistic identity stitching — web sessions + mobile installs + email + phone resolved to one canonical user_id" — code is `(orgId, projectId, externalId)` match only. No fingerprint + probabilistic match. (2) "Computed traits — first_seen / ltv / last_order_at / signin_count — calculated server-side" — no computed-trait engine; arbitrary trait storage only. (3) "Sync audiences to ad networks — Meta / Google / TikTok refreshed nightly" — zero ad-network sync code anywhere in the repo.
  • Real shape: nested AND/OR audience builder with query-time membership evaluation, `estimateAudienceSize` endpoint, anon→identified device-takeover, AI lifecycle classifier (`new` / `active` / `at_risk` / `churned`) under `ai_lifecycle_classifier_enabled` opt-in.
  • Segment comparison rewritten honest. New rows specifically call out cross-device stitching, computed traits engine, ad-network destinations — all marked ❌ for Sendora. whyStay LEADS with these missing features so prospects who need them self-disqualify.
  • AI lifecycle classifier added as a new row showing Sendora ✅ vs Segment ❌ — surfacing the real win that was buried before.
2026-05-30Docs
Auth Service audit clean + new Clerk + Firebase Auth comparisons
  • Deep audit of `apps/backend/src/modules/auth-service/` vs `AUTH_FEATURE_BULLETS` (shared/src/constants/auth-features.ts) — every claim verified in code. Passkeys live on web + iOS + Android (`SendoraCloudPasskeys.swift` + `SendoraCloudPasskeys.kt` + `sdk-web/src/auth.ts`). SCIM 2.0 covers Users + Groups CRUD (routes.ts:1539-1643). Hash import auto-detects bcrypt + scrypt + argon2 prefixes (crypto.ts:107-108). SSO + SCIM entitlement-gated to Business+ (entitlements.ts:68). First module in the integrity sweep with NO retracts — copy and code already match.
  • Auth0 comparison cleaned: pricing line updated to Auth0 B2C Essentials' actual ~$700/mo at 10K MAU (was "$240+/mo at 1K MAU" which was a different tier). Bulk-import FAQ corrected: endpoint lives in Auth Service, not Data-IO (caught from Wave 5 retract).
  • New **Clerk comparison** — modern auth alternative. Honest about Clerk's `<SignIn />` prebuilt components being a real win Sendora doesn't ship. Honest about Clerk's 20+ social providers vs Sendora's 7. Honest about Clerk's free 10K MAU tier.
  • New **Firebase Auth comparison** — Google ecosystem play. Honest about Sendora explicitly NOT shipping phone/SMS auth (out of scope), realtime DB, or full App Check. Honest about Firebase's free 50K MAU tier being hard to beat as a standalone auth choice.
2026-05-30Docs
Contact Widget — no drop-in widget.js bundle / no conversation analytics / no session-timeline attach
  • Audit of `chatbot/reply.ts` + `chatbot/routes.ts` + repo-wide widget search surfaced 3 over-claims. (1) "One-script-tag embed, ~12 KB gzipped" — there is NO widget.js bundle anywhere in the repo. The chatbot module ships a public submission endpoint + an SDK helper; the UI is on the customer. (2) "Visitor's full session timeline attached" to tickets — only event emit + `ai_traits` fire on inbound; no automatic timeline attachment. (3) "Conversation analytics — message volume, AI deflection rate, human-handoff rate, queue metrics" — stats endpoint returns `totalBots` + `totalSessions` only (line 77 of routes.ts).
  • Real shape: public `POST /chatbot/message` creates a `support_tickets` row + emits `ticket.created_via_widget`. AI mode does RAG via `ILIKE` over `kb_articles.title + body`, top-5 retrieved + re-ranked, top-3 fed as context to gpt-oss:20b on Ollama Cloud. Canned fallback when AI off OR LLM unavailable. Citations returned. `ai_traits` fires on inbound. SDK helpers on Web + RN.
  • Intercom comparison rewritten honest. New rows for prebuilt widget bundle, session timeline attach, conversation analytics — all marked ❌ for Sendora with specifics. whyStay LEADS with the missing features. 3 FAQs rewritten openly admitting the gaps + naming where Intercom is still the right answer.
  • Real wins kept + emphasised: RAG over your KB, ticket creation in same Support inbox, AI traits on inbound, per-org enable + env kill-switch, citations, canned fallback so the chat never 500s.
2026-05-30Docs
Surveys — auto-trait write / detractor flow / NPS trend reports retracted
  • Audit of `surveys/service.ts` + `createSurveySchema` surfaced 6 over-claims. (1) First-class CSAT/CES templates — only generic `rating` exists. (2) Conditional branching / logic jumps — no branching field in the question schema. (3) Responses auto-write Customer traits — `submitResponse` only writes to `survey_responses` + emits `survey.completed`; no profile trait write. (4) Detractor auto-flow (nps≤6 → save attempt) — not built; would require a customer-side Automation workflow. (5) Promoter auto-flow (nps≥9 → referral) — same. (6) NPS trend reports with cohort breakdown — `averageNps` is a placeholder field returning `null` in `getResponseStats` (line 98 of service.ts: `// computed from NPS question answers in production`).
  • Real shape: 6 question types (text/rating/nps/MC/SC/boolean), audience-trigger config, schedule window, response storage, `survey.completed` + `survey.started` events, SDK helpers on Web + RN.
  • Typeform comparison rewritten honest. New rows enumerate the gaps (no logic jumps, no payment fields, no file upload, no first-class CSAT/CES, no NPS trend reports, no built-in detractor flow). whyStay LEADS with the missing features. Pricing column adds concrete competitor tiers ($29 → $99 → $179/mo).
  • Product page now openly says: "Build the detractor flow yourself via the `survey.completed` event + an Automation workflow with `branch` + `update_profile` + `webhook` steps." Customers know what they're getting.
2026-05-30Docs
In-App Messages — A/B variants / frequency caps / Deep Links auto-wrap claims retracted
  • Audit of `createInAppMessageSchema` + `in_app_messages` table surfaced 3 over-claims: (1) A/B variants tied to Experiments with statistical readout + winner auto-promote — no `variants` field in schema, single content per message. (2) Frequency caps + holdout groups — no cap or holdout columns. (3) Click-through via Deep Links auto-wrap — `ctaUrl` is plain URL, no Links module integration.
  • Real shape: 4 surface types (banner / modal / slideout / tooltip), trigger by event + audience + URL + delay, light/dark theme, schedule window, priority, impression tracking. SDK helpers for Web + RN ship.
  • Pendo comparison rewritten honest. New rows for walkthroughs, frequency caps, A/B test variants — all marked ❌ for Sendora. whyStay LEADS with these missing features so prospects who need them self-disqualify before signup.
  • Tagline updated. useCases trimmed from 3 over-claims to 3 honest jobs (audience-targeted modals, empty-state nudges, time-boxed announcements).
2026-05-30Docs
Automation page — wait-for-event / holdouts / in-app step claims retracted
  • Deep audit of the workflow engine vs marketing copy surfaced 4 over-claims: (1) "Send across every channel — Email / Push / SMS / In-App / webhook" — In-App is NOT a step type, only 4 senders + 4 utility steps. (2) "wait-for-event" — only fixed-delay `wait`. (3) "holdouts" — no holdout splits in code. (4) "Per-step A/B with statistical readout — winners auto-promote" — A/B lives in Push/Email template layer, not at the step level.
  • Real shape: 8 step types implemented in `apps/backend/src/modules/automation/step-executor.ts` — `wait`, `send_email`, `send_push`, `send_sms`, `update_profile`, `webhook`, `branch`, `ai_action`. 8 starter blueprints in dashboard template gallery. Trigger model is `{ eventType, filters }` with wildcard matching — every other module's events flow through this one type.
  • Customer.io + Braze + Iterable comparisons rewritten with honest capability tables. Each `whyStay` list now leads with the missing features as the primary reason to keep the competitor (wait-for-event, holdouts, drag-and-drop canvas).
  • Real wins kept: AI step type (`ai_action` with generate/decide/extract via Ollama Cloud + BYOK), 8-template gallery, pause/resume, promote.failed webhook, real-time event triggers reading the same bus every Sendora module emits to.
2026-05-29Docs
Logs & API + Health moved into a Platform tier — not sold as standalone products
  • Marketing audit scored Logs & API (developer-tools) and Health (observability) at 2/5 on bundle-claim density — both read as generic infrastructure with no business-module value chain. They aren't standalone products; they're the developer surface + observability layer every other Sendora module rides on.
  • Added a new `Platform` category to the product taxonomy. Logs & API + Health move from Operate → Platform. Both gain a `platform: true` flag so /products + sidebar can render them differently.
  • Tagline + summary rewritten for both. The pitch is no longer "Stripe-grade DX" / "Datadog-grade observability" — it's "bundled platform infra, every paid plan, no add-on tier." Stop competing with Postman / Datadog Logs / Better Stack as standalone products; start competing on the bundle.
  • Sidebar + dashboard module registry left alone — these are still daily-driver pages for ops + engineers; the platform reframe is a marketing posture, not a product retirement.
2026-05-29Docs
Data Sync renamed Import & Export — biggest copy-vs-code gap in the audit
  • Fourth integrity gap surfaced. Marketing claimed: scheduled exports to S3 / GCS / Azure Blob / R2, reverse-ETL to Snowflake / BigQuery / Postgres pushed on cron, backfill jobs with safe-pause / safe-resume, bcrypt / scrypt / argon2 hash-import. Code shipped: CSV import of 3 entity types, on-demand export of 4 to CSV/JSON in R2, 15-min presigned download URL. No cron, no scheduled push, no destination connectors, no backfill resume. Hash-import lives in auth-service, not here.
  • Renamed to **Import & Export** in product page + dashboard sidebar + module registry. Honest about the scope: ad-hoc migration + ad-hoc data pulls, not continuous warehouse sync.
  • Hightouch comparison rewritten as a clear non-replacement. Hightouch wins on every destination + cron + connector row; Sendora's only win is "you may not need it at all if all your destinations live in Sendora". 5 FAQ entries explicitly call out what's missing.
  • useCases trimmed to the 3 honest jobs: migration on/off, ad-hoc analyst pull, periodic warehouse load you trigger yourself.
  • Scheduled-push + destination connectors moved to public roadmap (no committed date) so prospects evaluating against Hightouch see the gap upfront instead of in the contract.
2026-05-29Docs
Webhooks repositioned — one signed subscription to the whole platform's taxonomy
  • Webhooks product page rewritten around the bundle-only story: one HMAC-SHA256-signed subscription captures the whole 50+ event taxonomy across auth / push / email / sms / billing / support / csat / consent / attribution / automation.
  • Stack-math callout: Svix Pro is $490/mo for the delivery infra alone — you STILL ship product events into it. Hookdeck Mid is ~$200/mo. Sendora bundles both source + delivery.
  • Concrete event examples per module added inline (`auth.user_created`, `push.token_invalidated`, `email.bounced`, `csat.detractor`, etc.) so prospects see the taxonomy depth without a dashboard demo.
  • Svix comparison gains 4 stronger why-switch bullets including rotating-secret overlap + bulk replay. New 'Cost comparison' FAQ. whyStay list expanded for 10M+/mo volumes where Sendora's scale-target doesn't fit.
  • New Hookdeck comparison — honest about Hookdeck's visual transformation builder being a legit win Sendora doesn't ship.
2026-05-29Docs
Search page rewritten — honest about being ILIKE, not Algolia
  • Audit caught the third code-vs-copy gap. Search marketing claimed Algolia-style typo tolerance + synonyms + ranking-tuning UI. Code is Postgres `ILIKE '%term%'` across 4 fixed columns (`profiles.email/name`, `support_tickets.subject`, `kb_articles.title`, `events.event_type`) with hardcoded `score=1`.
  • Rewrote the product page to claim only what code does: 4-table federation, type-filter facet, zero-sync (data already in tenant), case-insensitive substring match.
  • Explicit non-features called out: no typo tolerance, no synonyms, no ranking tuning, no Custom Ranking, no Personalization, no A/B testing, no edge-eval latency.
  • Algolia comparison rewritten honestly — Algolia wins on every feature row; Sendora wins on the zero-sync + bundled-cost angle. 5 FAQ entries explicitly enumerate the missing features.
  • Dashboard tagline updated to match: "Substring match across profiles, support tickets, KB articles, and events — one query, zero sync code."
  • The win remains real for the right use case: ops command palette for in-tenant data where Postgres ILIKE is sufficient and per-record Algolia bills are wasted.
2026-05-29Docs
Privacy / GDPR repositioned as flagship — OneTrust comparison + Osano + migration guide
  • Privacy product page rewritten as flagship story: real send-time consent enforcement on email broadcasts (Wave 1a's gate), Art. 15 DSAR export + Art. 17 erasure across 7 tables enumerated in `gdpr/routes.ts`, recent-passkey-UV step-up on erasure, audit log on every privileged action.
  • OneTrust comparison strengthened with code-grounded facts (7-table cascade, `enforce_consent` toggle, `confirm="DELETE"` anti-fat-finger, 6 FAQ entries). Honest about what we DON'T ship — vendor-risk, third-party-risk, banner UI.
  • New Osano comparison ($199/mo Starter → $1,000+/mo Mid) — same posture pitch as OneTrust at the next-tier price.
  • New `/migrate/onetrust` guide — 7 numbered steps + 4 watch-outs. Honest about the banner gap and the `analytics` consent enforcement gap.
  • Cookiebot comparison gains 2 FAQs covering the recommended pattern: Cookiebot for the banner, Sendora for the enforcement + DSAR.
  • Fintech + healthtech industry pages now lead with OneTrust in their `topComparisons` (was 3rd / 4th).
2026-05-29Docs
Notifications module renamed Ops Alerts — copy now matches what the code does
  • Audit caught a code-vs-copy lie: marketing described the Notifications module as a Knock-style channel-agnostic notify() with preference center + per-channel fallback chains. The actual module ships Slack + HTTPS webhook routing for platform events (rule match → dispatch → retry → log). Different product.
  • Renamed to **Ops Alerts** across the marketing page, dashboard sidebar, and module registry. New category: Operate (was Engage).
  • The multi-channel fan-out story (Email + Push + SMS from one trigger) lives in Automation workflows, where the code actually does it — `step-executor.ts` calls each sender's service directly. The Knock comparison now points at Automation with honest gaps called out (no single-call `notify()`, no built-in preference center widget, per-module frequency caps instead of unified).
  • No backend change required — the existing channel API + retry worker are exactly the Slack/webhook router the new copy describes.
2026-05-29Platform
Consent module now actually gates outbound email
  • The Consent module shipped as a ledger — record grants, record revocations, audit trail. Marketing copy claimed it gated outbound senders. It didn't. As of migration 0057 it does.
  • New org-level toggle `enforce_consent` (default off — opt-in per tenant). Surface lives at the top of `/dashboard/consent`.
  • When on, broadcast + workflow email categories require a granted `marketing` consent row for the recipient before dispatching. No record OR most-recent record revoked → `email_sends` row written with `status='failed'`, `provider='suppressed'`, `metadata.suppressed_reason='no_consent'`. Visible in send stats + audit log.
  • Transactional, auth, ticket, and inbound-ack categories bypass the gate — those are platform-critical, not customer marketing.
  • Consent records gain a `phone` column + 3 partial indexes (`org_id, {email,user_id,phone}, purpose, created_at DESC`) so SMS + push gates are a small follow-up (those modules need a `category` discriminator on their send schemas first).
  • Backend tests 750/750 + 11/11 shared drift + 59/59 backend drift green.
2026-05-29Docs
Every product page rewritten — bundling-first positioning
  • All 22 product pages now lead with what only Sendora can offer because every module shares one tenant: same `user_id`, same audiences, same events, same workflow engine, one bill.
  • Each page now names the 2–3 competitors being replaced (Mixpanel + Amplitude, OneSignal + Braze, Auth0 + Clerk, Intercom Help Center, etc.) and reframes features as cross-module wiring instead of standalone capabilities.
  • Knowledge Base repositioned: one article auto-powers 4 surfaces — public help site (`sendoracloud.com/help/<orgSlug>`), AI chatbot RAG, in-app `kb.search()`, and Support ticket cross-link.
  • Customers repositioned: the CDP every other module reads from, real-time, no reverse-ETL. Replaces Segment + mParticle.
  • Authentication repositioned: identity wired to your product data. Replaces Auth0 + Clerk + the integration glue that follows them.
  • New `FeatureBullet` inline-markdown renderer (`**bold**` + `` `code` ``) — visual hierarchy across every product page without bespoke JSX.
  • Home page hero + `/products` hero rewritten around the platform-replacement narrative.
  • Public help-center surface launched — any org can serve their KB at `sendoracloud.com/help/<slug>` with SSR + 60s revalidate + view counters.
  • New `/why-sendora` page laying out the explicit point-tool-vs-platform comparison.
2026-05-29Platform
Device-takeover on signIn + global plan-gate UX + settings polish
  • Closed the duplicate-push class of bug architecturally. When a previously-anonymous device signs in to an identified account, Sendora retires the anon `user_id`, reassigns its push tokens, hard-deletes the anon user row, and emits `auth.device_takeover` — one device always resolves to one identity. Wired on every signin path: email-password, social (Google / GitHub / Apple / Microsoft / LinkedIn / Facebook / Discord), magic link, email OTP, passkey, MFA challenge, OIDC SSO, SAML SSO.
  • Customer-side cleanup signals: subscribe `auth.device_takeover` webhook to delete the matching row from your own users mirror (full server-pipeline path), OR register `auth.onDeviceTakeover(cb)` inline in your app code (zero infra). Inline path lives on `@sendoracloud/sdk-react-native` 1.0.5 + `@sendoracloud/sdk-web` 3.0.1 + iOS / Android 4.0.5. Full mechanism + examples at /docs/device-takeover.
  • Belt + braces — recipient-dedup at the messaging dispatch layer: email + SMS sends within a 60s window to the same `(orgId, recipient, content)` are suppressed and tagged with `status='suppressed'` + `metadata.suppressed_reason='recent_duplicate'` for audit trail.
  • Global TierGateModal — any 402 `tier_required` or 403 `ENTITLEMENT_ERROR` from the API now triggers a universal upgrade prompt instead of a silent failure. New `usePlan()` + inline `UpgradeNotice` primitives wired across Push (geofences / live-activities / templates / A-B tests) + Audiences pages.
  • Email-domain settings: BYOD plan gate is now surfaced inline (amber Growth+ callout + disabled input) instead of silent save() no-ops. Same posture across the dashboard — destructive paths emit toasts on every preflight failure.
  • Step-up wiring (Touch ID / passkey) on API-key revoke + Stripe billing portal button. 'Manage subscription' is hidden on Free plan + backend returns typed 409 `NO_BILLING_ACCOUNT` instead of opaque 500.
  • Settings sweep — Modules page deleted (read-only dead weight), Identity folded into API Keys as an Advanced card. Security page hardened — 8s `/auth/refresh` timeout + 10s Web Locks acquisition timeout + module-level toast singleton (root cause of an intermittent ~50 GETs / 12s infinite-loop on hot-reload).
  • Migration 0056 (SSO state rows carry `prev_anon_refresh_token`). Backend on `f90f302`. 742/742 tests + 11/11 shared-drift green throughout.
2026-05-29SDK
SDK majors — drop orgId from init + unprefixed /v1/<resource> API
  • @sendoracloud/sdk-web 3.0.0 + @sendoracloud/sdk-react-native 1.0.0 published to npm. iOS 4.0.0 + Android 4.0.0 tagged on the public SwiftPM / JitPack mirrors.
  • Breaking change: SDK init takes `{ apiKey }` only — `orgId` is now resolved server-side from the API key. Migration is one-line: drop the `orgId` field from your `Sendora.init({ ... })` call.
  • REST surface now exposes unprefixed routes — `POST /v1/events`, `GET /v1/profiles`, `/v1/links/...` etc. — alongside the legacy `/orgs/:orgId/...` paths. API-key auth auto-derives org + project, so the prefix is redundant.
  • OpenAPI 1.9.0 emits both shapes. The interactive Scalar explorer at /docs/openapi covers every endpoint with try-it-out + per-language code samples.
  • Every API key is now project-scoped — workspace-wide keys are no longer mintable. New mint UX defaults to public (`pk_*`) and warns when you flip to secret (`sk_*`).
  • Dashboard API-keys page surfaces UNUSED + STALE chips on the last-used column so dead keys are obvious at a glance.
  • /me endpoint now returns 8 fields for API-key auth (authType, keyId, name, kind, scopes, environment, orgId, projectId, lastUsedAt) — handy for SDK preflight checks.
2026-05-20Release
Onboarding wizard — Firebase-style polish + one-click test event
  • Stepper redrawn as numbered circles with connecting lines + checkmarks on completed steps.
  • Selected platform cards on the Apps step now use a primary-tinted highlight + check pill (no more washed-out white in dark mode).
  • SDK step surfaces your public key as a dedicated copy-able card above the install snippet — no more digging for it inside code.
  • Verify step adds a 'Send test event' button so you can flip the green check without writing any SDK code first. Also ships a curl one-liner with the key baked in for terminal-first users.
  • Inline tip on the React Native install tab clarifying Expo Go vs Dev Client — push token registration requires a Dev Client since Expo SDK 53.
2026-05-17Release
Observability — workflow run drill-down + audit / webhook filters
  • Workflow runs list gains status / workflow / date-range filters + a one-click 'Failed only' pill. Detail page surfaces the first failed step error in a banner and adds a Cancel-run button on running flows.
  • Audit log filters by resource type, actor UUID, and inclusive date range. Click any row to expand and inspect the full metadata JSON, IP hash, and actor type.
  • Webhook deliveries view adds filters (status / event-type prefix / endpoint) and expandable rows showing the full payload JSON, reconstructed request headers, and the full error response — Stripe / Svix parity.
  • Reusable row-expansion API on the DataTable + ResourcePage primitives so other modules can adopt the same drill-down pattern.
2026-05-16Release
11 product pages upgraded to provider-parity UX
  • Create link — Branch / Firebase / AppsFlyer parity: 14 fields under accordion sections, sticky preview pane (URL + social card mock), QR code on success, 3 follow-up CTAs.
  • Push compose + templates — OneSignal / Braze: sticky phone-mock lock-screen preview, character counters with iOS/Android truncation guidance.
  • Survey editor — Typeform / SurveyMonkey: side-by-side live preview with per-type answer mocks (NPS strip, 5-star, radio/checkbox).
  • Feature flag detail — LaunchDarkly / Statsig: targeting rules with rollout-% slider + per-rule value editor, env toggles, type-aware default editor.
  • Knowledge base article editor — Intercom / Help Scout: markdown toolbar + side-by-side preview, auto-slug, 100K character counter.
  • In-app messaging editor — Braze / Iterable / Pendo: device-frame preview across all 4 message types with light/dark theme.
  • SMS compose — Twilio / MessageBird / Plivo: iMessage-style mock, GSM-7 / UCS-2 auto-detection with live segment counter, TCPA reminder.
  • Email compose — Mailchimp / Resend / Sendgrid: template picker, sender / reply-to overrides, sandboxed iframe inbox preview.
  • A/B test variant editor — OneSignal / Braze: per-variant rows (key / weight / title / body / image), 'Even split' shortcut, live weight-sum indicator.
  • Live Activities ContentState — Apple ActivityKit: visual key/value editor with type select, Visual / JSON mode toggle.
2026-05-16Platform
Push A/B test winner declaration + workflow template gallery
  • Conclude any A/B push test from the dashboard — pick the winning variant, backend validates the key against the test's variants, dashboard surfaces a 🏆 badge + concluded date.
  • Workflow template gallery (/dashboard/automation/templates) ships with 8 curated starters: welcome series, cart abandonment, order confirmation + receipt, post-purchase feedback, trial ending, 30-day re-engagement, birthday greeting, password reset. One click clones into a new (inactive) workflow ready to edit.
2026-05-08Release
Mixpanel-parity power-user analytics
  • Event detail page — sparkbars + Property / Trait tabs + auto-discovered key dropdowns. Drill into any tracked event without writing SQL.
  • Funnel detail with per-step bars + drop-off table.
  • Group-by user trait (joins events × profiles) — break any time-series down by plan / industry / company size.
  • Compare events on time-series with multi-line SVG chart (/dashboard/analytics/compare).
  • Saved Insights / Boards (/dashboard/analytics/boards) — bookmark + share any chart configuration. New migration 0043.
  • Cohort filter via audienceId wired into top-events, time-series, and compare endpoints — bound any insight to a saved audience.
2026-05-08Platform
Pagination + dashboard dark theme + how-to-integrate tabs
  • Pagination UI shipped across every list view — 10 / 50 / 100 selector, Prev / Next, total range counter. Backend `?withTotal=0` opt-out saves ~50% of COUNT(*) queries on deep paging.
  • Dashboard dark theme rolled out platform-wide. New `:root` palette + `.notice` utility classes replaced 17 hardcoded light pastels (Attribution / Push warnings now readable in dark mode).
  • 'How to integrate' tab added to every product module — 11 new snippet files + 15 integration pages so engineers can copy a working SDK call directly from the module they're configuring.
2026-05-07Release
Auth redesign — modern sign-in / sign-up / forgot / reset pages
  • New auth-card primitives, mesh gradient brand panel restricted to the dashboard indigo palette.
  • Backend `/auth/password/forgot` + `/auth/password/reset` endpoints wired through SuperTokens + the shared Cloudflare Email Service.
  • HIBP breach check on password reset — refuses common compromised passwords.
2026-05-06Platform
SCIM 2.0 user + group provisioning (Business+)
  • Provision users and groups directly from your IdP — Okta, Azure AD, JumpCloud, OneLogin, Rippling, Workday.
  • Mint a bearer token in dashboard → Authentication → SCIM, paste it into your IdP's SCIM connector with the base URL.
  • RFC 7643 schemas + RFC 7644 protocol. Supports User + Group CRUD, PATCH ops (active, displayName, externalId, name, member add/remove), filter by `userName eq` / `externalId eq`.
  • Plan-gated to Business and above.
2026-05-06Platform
SAML 2.0 SSO (Business+)
  • Per-org Service Provider keypair generated on first config save (RSA-2048, AES-256-GCM at rest).
  • SP metadata XML at /auth-service/sso/saml/:orgId/metadata for one-click IdP import.
  • Real self-signed X.509 certificate in metadata so spec-strict IdPs (Microsoft Entra) accept it directly.
  • Signed AuthnRequests, verified-signed Assertions, InResponseTo replay defence.
2026-05-06Platform
Authentication: passkeys, magic link, email OTP, MFA
  • WebAuthn passkeys on web, iOS, and Android.
  • Magic link sign-in + 6-digit email OTP (cross-device, 5-min TTL).
  • TOTP MFA (RFC 6238) with recovery codes; per-user enrollment.
  • Apple Sign In + Google + GitHub OAuth.
  • Bulk hash import — bcrypt, scrypt, argon2 — for migrating off Auth0 / Clerk / Firebase without forcing a password reset.
2026-05-06Platform
SMS STOP / START compliance (TCPA + CTIA)
  • Inbound Twilio webhook honours STOP / UNSUBSCRIBE / CANCEL / END / QUIT — recipient is auto-suppressed across the org.
  • START / UNSTOP / YES re-subscribes the same number.
  • Per-org suppression list visible on /dashboard/sms with manual remove.
  • Outbound to a suppressed number is recorded as `failed` instead of dispatched.
2026-05-06Platform
Visual workflow canvas
  • Drag-to-reorder steps with native HTML5 drag handles.
  • Per-step icons + colour bars: email, push, SMS, profile, webhook, branch.
  • Branch step previews If true / If false outcomes inline.
  • Click-pill palette to append a new step of any type.
2026-04-19Platform
Email starter templates + template editor polish
  • 5 curated starter templates on the Email → Templates page — welcome, password reset, receipt, re-engagement, support response. One-click import.
  • Template editor gains a live-preview pane so you can see the rendered email while editing HTML.
2026-04-19Security
Homegrown bot protection on the Contact Widget
  • 4-layer bot check replaces external CAPTCHA dependency: honeypot field, minimum fill time, HMAC-signed challenge, behavioral signals.
  • No per-hostname registration overhead — the widget works on any customer site out of the box.
  • All failures collapse to a single generic error so attackers can't probe for which layer caught them.
2026-04-19Platform
Reputation-safe multi-tenant email
  • BYOD (Bring Your Own Domain) is now mandatory for customer-authored email — Email module API, workflow email steps, broadcasts. System mail (auth, billing, support auto-reply) still runs on Sendora Cloud.
  • Per-org burst rate limit (60 sends/min, 10/min during the 30-day probation period for new accounts).
  • Content safety scanner blocks phishing, credential-harvest, and pharma/crypto-scam patterns.
  • RFC 8058 one-click unsubscribe + suppression list enforced across every send path.
  • Dedicated `Settings → Email` page with step-by-step DNS setup wizard and live reputation dashboard (volume, bounce rate, complaint rate against SES thresholds).
2026-04-19Platform
Admin cross-org email reputation view
  • New `/admin/email-reputation` page surfaces every org's 7-day + 30-day sending health in one table.
  • Sorted by composite risk score so orgs crossing SES thresholds (5% bounce / 0.1% complaint) float to the top.
  • Paired with docs/RUNBOOK-abuse.md covering the full triage + response workflow.
2026-04-18Platform
Multi-tenant support email, BYOD, and reply threading
  • Support ticket agent replies now send From the customer's verified domain when BYOD is configured, or a plus-addressed Sendora Cloud address otherwise.
  • Reply-To threading routes customer replies back to the originating ticket — no email client quirks to worry about.
  • Cloudflare Email Routing + a dedicated email-worker power the inbound pipeline: `support+<slug>@sendoracloud.com`, `t_<token>@sendoracloud.com`, and `[Ticket <id>]` subject-line fallback all work.
  • New ticket detail page in the dashboard with thread view, internal-note toggle, status actions, and agent reply.
2026-04-18Platform
R2-backed Data-IO exports
  • Exports over 2 MB now stream to Cloudflare R2 instead of loading into memory.
  • Download endpoint 302s to a presigned R2 URL.
  • Large customer-data exports (up to millions of rows) work without backend memory pressure.
2026-04-18Platform
Real workflow step dispatchers
  • Workflow `send_email`, `send_push`, `send_sms`, `update_profile`, and `webhook` steps execute for real with full template interpolation.
  • Workflow run detail page shows the trigger context + each step's outcome.
  • Branch step actually branches (true/false paths jump to different step targets).
2026-04-17Platform
Stripe billing live (test mode)
  • End-to-end subscription flow: checkout → webhook → plan update → entitlements.
  • Ingest pipelines wired: webhooks, workflows, profile upsert, data-io.
  • Real APNs + FCM push delivery (not just scheduled).
  • 4 dashboard visual editors for workflows, audiences, templates, and forms.
2026-04-17Security
Compliance + security scaffolding
  • Session cookies moved to httpOnly — no more localStorage token exposure.
  • CSP + HSTS headers shipped.
  • HIBP breach check on password creation, CSRF double-submit, webhook retry with exponential backoff.
  • GDPR export + delete endpoints, destructive-operation friction, Dependabot, security.txt, subprocessors page.
2026-04-17Platform
Marketing site live
  • Public marketing site launched at sendoracloud.com.
  • Full product catalog across all 22 modules.
  • Cmd+K search, multi-language code samples, and a live pricing calculator.
2026-04-16Platform
Dashboard deployed to Cloudflare Workers
  • Dashboard deployed via OpenNext at app.sendoracloud.com.
  • 64-table schema pushed and seeded with plans and modules.
  • Dedicated IAM user for SES sending role.
2026-04-15Release
All 22 modules GA
  • Every product module is live behind unified APIs.
  • Web, iOS, and Android SDKs published as 1.0.
  • OpenAPI spec and Stripe billing in place.
2026-04-10Docs
Quickstart rewrite
  • End-to-end track + identify example with multi-language tabs.
  • Event envelope reference added.