Skip to content
Sendora Cloud
Create account
Architecture

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+):

  1. SDK forwards the anon refresh token on every identified-signin call when the local subject is anonymous.
  2. Backend validates the token, revokes the anon session, reassigns the device's push tokens to the identified user, hard-deletes the anon user row.
  3. Emits auth.device_takeover webhook + sets retiredAnonUserId on the signin response.
  4. SDK fires registered onDeviceTakeover listeners 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-web 3.0.1 — browser-side surface.
  • @sendoracloud/sdk-web-ssr 0.2.1 — HttpOnly-cookie SSR session helpers for Next.js / Remix / SvelteKit.
  • @sendoracloud/sdk-react-native 1.0.5 — RN 0.70+, Expo + bare RN.
  • sdk-ios 4.0.5 — SwiftPM, Swift 5.9+, iOS 15+.
  • sdk-android 4.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 in NO_ALIAS allowlist.
  • 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