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.
What’s stored where
Section titled “What’s stored where”| 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. |
What passes through our account
Section titled “What passes through our account”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
importsbucket.
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.
How a request reaches your data
Section titled “How a request reaches your data”- Your request arrives at your Region’s API host (
api.rhinorelay.comfor us-east-2, or your own API domain) through CloudFront and AWS WAF. - 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
421wrong_region: your data is only processed by our deployment in your Region. - 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
SourceIdentitynaming the function that opened it, so your CloudTrail shows which one acted. - 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.
How a campaign goes out
Section titled “How a campaign goes out”POST /campaigns/{id}/scheduleruns the pre-send checks and creates a one-time schedule in our account. Its payload is IDs only.- At the start time, the
stagefunction builds the audience in yourapptable, most engaged contacts first, and puts the first stage’s batches on our sharedsendqueue. Each message names your account, the campaign and the batch; the batch itself is in your table. - The
senderfunction takes each batch from the queue, reads the batch and its contacts from your tables, sends through your SES with therhino-relay-campaignsconfiguration 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. - 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
eventsqueue. Theeventsfunction applies them to send records, contacts and counters in your tables. - 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.
Check it worked
Section titled “Check it worked”/health shows whether we can reach your stack, and every other setup check.
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.