// Security architecture
How StellarReach is built.
The technical detail behind our security page — written for whoever has to sign the vendor off. If something you need isn't here, ask us and we'll answer directly.
Encryption at rest
Three separate layers
Everything moves over HTTPS/TLS. At rest, three different kinds of data are protected separately, and no key is ever used for two purposes.
Connected credentials — the API keys and OAuth tokens you connect (AI keys, Google Maps, CRM and telephony credentials, mailbox tokens) are encrypted with AES-256-GCM before they are written.
Your prospect data — contact email addresses and phone numbers, business addresses, rep notes, decision-maker contacts, captured reply text and the prospect scoring queue are encrypted at rest under a data key unique to your workspace. Your workspace's key is itself stored wrapped by a key-encryption key held in AWS KMS, so the ciphertext and the key that opens it never sit in the same place — and the key that opens it never sits in our environment at all.
Everything else — operational data that isn't personal (scores, timestamps, counters, settings) is stored in the clear, so the product can query it without holding keys open.
The key hierarchy
| Key | What it does | Where it lives |
|---|---|---|
| Key-encryption key | Wraps every workspace's data key. Never touches a data value. | AWS KMS. The key material never leaves the service — we call it to wrap and unwrap, and never hold it. |
| Workspace data key | One per workspace, 256-bit, generated at random. Stored only in wrapped form. | Database, encrypted. |
| Field key | AES-256-GCM on individual column values. Derived from the data key via HKDF. | Derived in memory, never stored. |
| Index key | HMAC-SHA256 for blind indexes (below). Derived separately so it can never decrypt anything. | Derived in memory, never stored. |
Each encrypted value is bound to your workspace id as additional authenticated data. A row lifted out of one workspace and dropped into another does not decrypt — it fails closed rather than leaking across.
Key access is logged and revocable
Because the wrapping key lives in a managed key service rather than in a configuration variable, every unwrap is an authenticated API call that leaves an audit record, and access can be withdrawn in seconds — which makes every workspace key unopenable immediately, without re-encrypting a single row, since the data keys themselves never change.
To be precise about the limit: this doesn't stop our own running application from decrypting, because it has to. What it changes is that the capability is online, recorded, and revocable — rather than a secret that, once copied, works forever and silently.
Blind indexes, so we don't need plaintext to match
AES-GCM is randomised: the same email address encrypts to a different value every time, which means the database cannot compare two addresses. That matters most for the thing you'd least want broken — the opt-out list. If a suppressed address couldn't be matched, someone who unsubscribed could be emailed again.
So alongside each encrypted address we store a keyed HMAC-SHA256 of its normalised form — a blind index. Equality checks (is this address suppressed? has this lead already been imported?) run against the index while the address itself stays encrypted. The index key is derived separately from the field key, so it can confirm that two values match without being able to reveal either one.
What that protects — and what it doesn't
The threat it's built for
Workspace encryption is aimed squarely at a database compromise: a leaked connection string, a database dump restored on someone's laptop, a read replica, a console session, a stolen backup. None of those alone yields a readable prospect book, because the key isn't in the database.
What we will not claim
It does not make us blind to your data, and we won't market it as if it did. Your automated senders fire while you're asleep, the scoring worker runs unattended, and your prospects' brief pages are served to people who aren't logged in. All of that needs plaintext with no human present, so the running application must be able to decrypt — and therefore so can whoever operates it.
The honest description is: encrypted at rest per workspace; operator access is possible, restricted, and auditable. Not "we can't read your data." Any vendor running unattended automation on your behalf who tells you otherwise is worth a second question.
Deletion is cryptographic
Deleting a workspace destroys its data key. Every value that workspace ever
encrypted becomes permanently unreadable — including in backups
taken before the deletion. That is an erasure guarantee a
DELETE statement cannot give you while old dumps still exist,
and it's the part of this design we'd point an auditor at first.
Isolation & access
Row-level workspace scoping
Every table carrying customer data is scoped by workspace id, and queries are issued through repositories that apply that scope rather than leaving it to each caller to remember.
Isolation enforced twice
Scoping is the first line; per-workspace keys are the second. Even a query that escaped its scope returns values encrypted under a key bound to a different workspace, which won't open.
Password storage
Salted, memory-hard scrypt hashes with constant-time verification. Plaintext passwords are never stored and never logged.
Single sign-on
Sign in with Google, GitHub, or Microsoft instead of a password, so account lifecycle stays with your identity provider. SSO signups are password-less by design.
Roles and per-seat controls
Admin and member roles gate the back office, with platform-only controls separated from workspace admin controls. Individual AI capabilities can be toggled per seat.
Separate billing boundaries
If you invoice your own clients through the platform, that runs on your Stripe account and your webhook secret — deliberately sharing nothing with our billing, so your revenue never lands in our ledger.
Your mailboxes and connected accounts
We ask for the narrowest scope that does the job
Sending email needs a send scope. Reading replies out of a Gmail mailbox needs a restricted read scope — a much broader permission, and one that would let us read mail unrelated to your outreach. We didn't want that permission, so for Gmail and SendGrid we don't take it.
Instead, reply capture is optional and works without mailbox access: your
outreach carries a per-lead tokenised Reply-To address, the
prospect's reply arrives at that address, and it's matched back to the
lead and forwarded into your real inbox. We see replies to our own outreach
and nothing else in your mailbox. (Microsoft 365 offers a genuinely narrow
read scope, so there we read the mailbox directly if you prefer.)
Token handling
OAuth refresh tokens are encrypted at rest. Microsoft rotates refresh tokens on use, so each run refreshes once and persists the replacement. A daily health check exercises every connected inbox's tokens and alerts you when a grant has expired or been revoked, so a dead connection surfaces as a warning rather than as silently unsent email.
You can disconnect any mailbox, CRM, telephony or AI credential from Settings at any time; revoking the grant at the provider works too, and the health check will tell you it happened.
Outbound request safety
The product fetches prospect websites to score them and build demos, and those URLs come from map results, CSV imports and hand-edited leads — in other words, attacker-influenceable input pointed at our own network position. This is the classic server-side request forgery setup, so every such fetch goes through one guarded path that does three things a URL allowlist doesn't:
- Rejects non-public addresses after resolution — IPv4 and IPv6, including the IPv4-mapped and NAT64 spellings that bypass naive checks.
- Re-validates every redirect hop. A public host answering
302 → http://127.0.0.1/is the standard way around a first-hop-only check. - Pins the socket to the address it validated, so a DNS answer that changes between the check and the connection has no second resolution to poison.
Fetched page content is treated as untrusted input to the generation step, never as instructions.
AI processing
Prospect scoring, demo building and email drafting call Anthropic's API. What's sent is the prospect's own public website content plus the lead fields the task needs — not your whole database, and not your other workspaces. Anthropic is named as a sub-processor in our Privacy Notice and covered by our DPA.
On Scale and Agency you can bring your own AI key, in which case those calls run under your own provider account and your own terms, and we never hold the key in plaintext. Either way, a hard per-workspace monthly budget cap pauses AI before spend can run away, and per-seat toggles control which team members can use which AI helpers.
Platform & supply chain
One runtime dependency
The application ships with exactly one third-party runtime dependency: the PostgreSQL driver. The AWS request signer, the Stripe client, the email provider clients and all cryptography are written directly against documented APIs and Node's own crypto library rather than pulled in as packages.
That is a deliberate trade — more of our own code to maintain, in exchange for a supply-chain surface that is one package deep. Anything running inside the application can read its environment, so every dependency added is another party who could reach the keys. We'd rather write the signer.
Hosting and sub-processors
The application runs as serverless functions on Netlify with a managed PostgreSQL database. Every third party that touches your data — hosting, database, AI, email delivery, payments, and any CRM or telephony provider you choose to connect — is named in our Privacy Notice. We don't sell data, and a Data Processing Agreement is available for customers who need one.
Webhooks and inbound requests
Every inbound webhook is signature-verified before it is acted on — payments (HMAC), telephony status callbacks (HMAC or Ed25519 depending on provider), and inbound mail. Payment events are idempotent and deduplicated, and a write we couldn't record returns an error so the sender retries rather than us acknowledging a payment we didn't store.
Data lifecycle
Export
Your lead pipeline exports to CSV from Reports in the back office. Email support@stellarreach.app if you need a full-account extract in a particular format.
Deletion
Deleting your workspace destroys its data key, which makes its encrypted data unreadable everywhere it exists, backups included. Retention specifics are in the Privacy Notice.
Your prospects' rights
Opt-outs are enforced by a suppression list matched on blind index, so a person who unsubscribes stays suppressed even though their address is stored encrypted.
Payment data
Card details go to Stripe and are never seen or stored by us. We hold a customer reference and subscription state, nothing more.
Responsible disclosure
Found something? Email support@stellarreach.app with details and steps to reproduce. We'll acknowledge it, keep you posted, and fix confirmed issues promptly. Please give us a reasonable window before disclosing publicly, don't access data that isn't yours, and don't run tests that degrade the service for other customers.
Need something this page doesn't cover?
Send us your security questionnaire, or ask a specific question — a person will answer it.
Contact us