Skip to content

How it works

Our account holds the code, your account’s configuration, and work queues that carry IDs only. Your account holds all your data. Our functions assume one role in your account for every read and write, so your contacts and history never rest in our account.

How Rhino Relay reaches your AWS accountOur AWS account runs the code: CloudFront and WAF, the Lambda functions with one execution role, an event bus whose rules strip events down to IDs, three work queues shared by all clients that carry IDs only, EventBridge Scheduler and a tenant registry with no contact data. Your AWS account holds the data: an IAM role, the DynamoDB tables, Amazon SES, EventBridge rules, the S3 buckets, Firehose and Glue, and an optional CloudTrail trail. Our functions assume your IAM role, with your external ID, for every read and write. Your EventBridge rules forward SES events and CSV upload notices to our event bus. Your contacts and history are never stored in our account.Our AWS account, in your RegionCloudFront and AWS WAFAPI, custom and tracking domainsLambda functions, one roleapi, stage, sender, events, importer, daily…Event bus and rulesStrip your SES events down to IDsSQS: send, events, importShared by all clients; IDs onlyEventBridge SchedulerCampaign stages; payloads are IDs onlyTenant registryAccount, Region, role, entitlement; no contactsAssumes your role forevery read and writeEvents, IDs keptYour AWS accountIAM role rhino-relay-access-…Trusts our service role, with your external IDDynamoDB: contacts, appContacts, consent, campaigns, send recordsAmazon SESYour domain, quota and reputationEventBridge rulesForward SES events and CSV uploads to usS3: imports, exports, archiveCSV uploads, exports, raw SES eventsFirehose and GlueFull SES events to your archive, for AthenaCloudTrail (optional trail)Every session we open, by function
Data Your account Our account
Contacts, fields, tags, consent, scores contacts table —
Templates, segments, campaigns, send records, counters, imports, settings app table —
Raw event archive, CSV uploads, exports S3 buckets —
Mailchimp credentials during an import SSM parameter (SecureString) —
Idempotency records app table —
Work waiting to be done: batches to send, SES events to apply, imports to run — Three SQS queues shared by all clients. Each message holds IDs only: your account, campaign, batch or event. Never an address or any content.
Account registry: your AWS account ID, Region, role ARN, external ID, entitlement, resource names — DynamoDB
API key hashes and the keys that sign unsubscribe links — DynamoDB and Secrets Manager
Console users (your staff, not your contacts) — Cognito
Schedules for campaign stages — EventBridge Scheduler, with only IDs in each payload
API request log for the console (time, key, method, path template, status, latency, request ID) — DynamoDB, kept 14 days. Never bodies or addresses.
Logs — Account, campaign and request IDs and counts only. Never a contact ID, address or message ID.

Some data passes through our functions in memory, and is never written anywhere on our side:

  • contact records while we render and send your emails,
  • Mailchimp data while we import it into your tables,
  • CSV files while we read them from your imports bucket.

SES events reach our event bus whole, because EventBridge can’t trim an event on its way to another account. Our rules on the bus keep only IDs, timestamps and flags before the event reaches our queue, and they have no dead-letter queue, so a full event is never stored in our account. See What the template creates.

CloudFront request logging is off for the API and tracking domains. AWS WAF logs only blocked requests, with the path, query string and headers redacted. No endpoint takes contact data in a URL: looking up a contact by email is POST /contacts/lookup.

  1. Your request arrives at your Region’s API host (api.rhinorelay.com for us-east-2, or your own API domain) through CloudFront and AWS WAF.
  2. Your API key, or the AWS account that signed the request, picks your account before any handler runs. IDs in the path or body can never pick a different one. A request for an account in another Region gets 421 wrong_region: your data is only processed by our deployment in your Region.
  3. The function handling the request assumes your access role with your external ID. All our functions share one execution role, and each session carries a SourceIdentity naming the function that opened it, so your CloudTrail shows which one acted.
  4. It reads and writes your tables and buckets, and sends through your SES.

Credentials are cached in memory for each account and function, refreshed before they expire, and never shared between accounts.

  1. POST /campaigns/{id}/schedule runs the pre-send checks and creates a one-time schedule in our account. Its payload is IDs only.
  2. At the start time, the stage function builds the audience in your app table, most engaged contacts first, and puts the first stage’s batches on our shared send queue. Each message names your account, the campaign and the batch; the batch itself is in your table.
  3. The sender function takes each batch from the queue, reads the batch and its contacts from your tables, sends through your SES with the rhino-relay-campaigns configuration set, and records each outcome in your table. Each email is sent at most once, even when a step runs twice. Your batches go out one at a time, paced to your SES rate, while other clients’ batches run alongside.
  4. SES events go from your default event bus to your archive bucket (in full), and to our event bus. Our rules keep only IDs, timestamps and flags, and put them on our events queue. The events function applies them to send records, contacts and counters in your tables.
  5. Before each later stage, bounce and complaint rates are checked. If either is too high, the campaign pauses itself. See Campaigns and stage plans and Pauses.

CSV imports work the same way: your imports bucket tells EventBridge about the upload, your stack forwards the notice to our bus, and our importer reads the file straight from your bucket. See CSV import.

If one client’s messages flood a queue, we set that client’s messages aside and carry on for everyone else. See flood protection.

/health shows whether we can reach your stack, and every other setup check.

Terminal window
curl "$BASE/health" -H "Authorization: Bearer $KEY"

Check it worked: the connection check shows "ok": true and tenantState is connected.

We run in us-east-2 today and add Regions on request. See Regions and limits.