How Sendora is built
Sendora's pitch is that bundling 22 modules on one tenant unlocks cross-product wiring point tools can't deliver. The pitch only works if the platform is real. Here's what's under the hood.
One event stream
Every module reads from one Postgres-backed event store. Canonical envelope: type + userId + traits + properties + timestamp + context. Same envelope on every SDK (Web / SSR / RN / iOS / Android).
Modules reading the stream:
- Analytics — funnels, retention, group-by, compare, Saved Insights, Boards.
- Customers — audience-build queries over the same events.
- Automation — triggers fire off the stream in real-time, never sync-delayed.
- Support — ticket timeline = events filtered by user_id.
- Attribution — installs + downstream conversions joined natively.
- AI Chatbot — context retrieval from events + KB articles.
- Webhooks — outbound delivery for every product event in the canonical taxonomy.
One identity primitive
user_id resolves the same person across web + iOS + Android + RN + SSR. The hard part — anon → identified flip on the same device — is handled by device-takeover (s58.111+):
- SDK forwards the anon refresh token on every identified-signin call when the local subject is anonymous.
- Backend validates the token, revokes the anon session, reassigns the device's push tokens to the identified user, hard-deletes the anon user row.
- Emits
auth.device_takeoverwebhook + setsretiredAnonUserIdon the signin response. - SDK fires registered
onDeviceTakeoverlisteners so host apps can clean their own user mirrors.
Full mechanism: /docs/device-takeover.
Five typed first-party SDKs
Same release cadence as backend. Consistent contracts.
@sendoracloud/sdk-web3.0.1 — browser-side surface.@sendoracloud/sdk-web-ssr0.2.1 — HttpOnly-cookie SSR session helpers for Next.js / Remix / SvelteKit.@sendoracloud/sdk-react-native1.0.5 — RN 0.70+, Expo + bare RN.sdk-ios4.0.5 — SwiftPM, Swift 5.9+, iOS 15+.sdk-android4.0.5 — JitPack, Kotlin 1.9+, minSdk 26.
Drift-test gates
Every release is gated on parity tests asserting marketing copy, OpenAPI spec, shared types, and module registry stay in sync with backend reality. Concrete examples:
- openapi-coverage drift — every mounted Hono route is either in the OpenAPI spec or in an explicit opt-out allowlist. New surface area can't ship undocumented.
- alias drift — every
/orgs/:orgId/*route has an unprefixed alias for API-key auth or is inNO_ALIASallowlist. - internal-name lint — user-facing docs / marketing copy leak no Postgres table names. Table names are implementation detail subject to renaming.
- SDK scope drift — public method signatures across the 5 SDKs are equivalent.
- Form-bounds drift — dashboard form length limits match backend Zod schemas.
If a release would diverge a doc claim from backend behaviour, the gate fails before the PR lands.
BYOD reputation isolation
Customer-authored email ships from your DKIM-verified domain (real Resend Domains integration with Sendora-side DKIM keys + SPF + DMARC records). System messages — auth confirmations, password resets, billing receipts — ship from Sendora's own pool. Implication: a noisy tenant's marketing complaints can't tank your password-reset deliverability.
New senders run a 30-day probation pool. Per-org burst rate limits apply. Content safety scanner blocks obvious phishing / credential-harvest patterns at dispatch.
Platform-layer privacy enforcement
Consent state lives on the Customer profile. Every module reads it before acting:
- Email won't send to non-consenting recipients.
- Push won't notify.
- Analytics won't track non-consented events.
- Workflows won't enroll users whose consent was revoked.
DSAR / right-to-erasure cascades across every Sendora table (events, profiles, sessions, push tokens, sends, surveys) in one operation. Data residency picks EU or US per-org at sign-up; data stays in-region. Audit trail of every consent change is a first-class log surface.
Cross-module event taxonomy
50+ canonical event types covering every module. Picked verbatim from packages/shared/src/taxonomy/webhook-events.ts:
auth.signed_up auth.signed_in auth.device_takeover auth.mfa_enrolled email.sent / .delivered / .opened / .clicked / .bounced / .complained push.sent / .delivered / .opened sms.sent / .delivered / .failed link.created / .opened attribution.installed survey.completed / .response support.ticket_created / .status_changed automation.run_started / .step_completed / .promote_failed billing.subscription_created / .renewed / .cancelled … 30+ more
Customers subscribe to whatever they care about. Outbound delivery is HMAC- SHA256 signed with the Stripe convention (t=…,v1=… header) + exponential backoff (2s / 8s / 30s / 90s) + replay on demand.
Dogfooded by Pulse News
Pulse News (consumer news + social, iOS / Android RN app) runs 11 Sendora modules in production: Authentication, Customers, Analytics, Push, Email, Automation, Deep Links, Attribution, Support, Knowledge Base, Privacy.
Real bugs we hit during Pulse's use become platform-wide fixes — for example, the duplicate-push class of bug led to device-takeover landing architecturally across every customer, not a Pulse-specific patch. The kind of cross-product win point-tool stacks can't deliver structurally.
See /customers/pulse-news for the full case study.
Where to read more
- Event model — the envelope every module reads.
- Identity & anti-spoof — HMAC-signed identify() for authenticated products.
- Device-takeover on signIn — anon → identified mechanism.
- API reference — handwritten walkthrough.
- OpenAPI explorer — Scalar try-it-out over the live spec.
- Webhooks — canonical event catalog + HMAC verify + retry policy.