Multi-Tenant · UGC · Abuse Engineering

Your product is a distribution channel, whether or not you designed it to be

Anything that accepts a URL from one tenant and renders it for another is a link-delivery system with your name on the envelope. This page is about putting a hostname check into the signup, invite, profile, share and render paths server-side — and about the latency arithmetic that decides whether you call an API or hold the list in memory.

Where a hostname enters your product
SignupEmail domain, company URL, referral source
InviteAddresses you will email on a tenant's behalf
Profile & workspaceBios, website fields, custom link blocks
Content & file shareComments, tickets, public share links, embeds
Webhooks & redirectsCallback URLs and any ?next= parameter
390,000+Live, DNS-verified phishing domains
Sub-50msTypical API response time
100Domains per /batch request
10 req/sRate limit per API key
User-generated content
Signup abuse
Tenant account takeover
Open redirectors
Your own login page
Trust & safety tooling
Home / Use Cases / SaaS Platforms
The surface you built by accident

Every field that accepts a link is an email you send on somebody else's behalf

Multi-tenancy means one tenant's content is rendered inside another tenant's trust boundary — and frequently inside a notification email carrying your domain.

Product teams generally understand that a comment box accepts text and that text can contain a URL; what gets missed is how many of those boxes exist by the time a platform is a few years old — display names, workspace names, bios, custom status messages, ticket bodies, task descriptions, file names, folder descriptions, calendar event locations, form-field labels, signature blocks, saved-view titles, integration nicknames, and the free-text "reason" attached to nearly every approval workflow ever shipped.

The mechanic

Your notification email is the delivery vehicle

When a tenant adds a malicious link to a task and the assignment triggers a notification, your product has just delivered a phishing link to a mailbox, authenticated with your sending domain, past the recipient's filters, into a message the recipient has been trained by you to trust. They do not experience it as a stranger's link — they experience it as your product telling them something needs attention, and the deliverability you spent years earning is precisely what makes it effective.

Beyond email

Every render context inherits your address bar

A public share page on your domain displaying a tenant's content, an embed widget your customers put on their own marketing sites, a status page, a changelog, a community forum, a public API playground with saved examples, a marketplace listing for a third-party integration — each puts a hostname somebody else chose in front of an audience that reads your domain in the address bar and draws the obvious conclusion.

The framing

Treat a hostname as an input, not as content

None of this means user-generated content is a mistake or that the answer is to strip links. It means a hostname arriving from outside your trust boundary should be validated like any other input, at the point of entry or the point of render rather than in a moderation queue somebody reads on Tuesdays — which converts a class of abuse from "somebody has to notice" into "the platform already knows".

Who this is for

Written for the engineer, not the buyer

The rest of this page covers where the check goes in your code, what it costs in latency, when to hold the whole database in memory instead of calling anything, what the check genuinely does not tell you, and how the same data supports the tooling your trust and safety team already wishes it had.

Integration points

Five code paths, one rule about where the call lives

Each of these is a place a hostname crosses from outside your system to inside it. Screening at the crossing is cheaper than screening everywhere downstream.

Signup and email domain

At account creation you already have a hostname — the part after the @, plus whatever company URL the form collected. Checking both costs a single credit and gives you a durable signal to attach to the tenant record, long before any content is created.

Invite and member-add

An invite is your platform agreeing to email a stranger. Screen the recipient's domain before you send, because a bulk invite to hundreds of addresses on hostile infrastructure is both an abuse vector and a fast way to damage your sending reputation.

Profile and workspace links

Website fields, bio links, custom link blocks and workspace metadata are edited rarely and rendered constantly. Check on write, store the verdict alongside the value, and re-check on a schedule rather than on every page view.

Share links and embeds

Anywhere your product generates a public URL or accepts one for an embed, extract the hostnames and screen them. This is also where /batch pays for itself: a document with forty outbound links is one request, not forty.

Render and preview generation

If you unfurl links into previews, you are already fetching metadata — add the hostname check to the same job. A flagged domain gets a warning card and no thumbnail instead of a rich preview that makes the destination look endorsed.

The one rule that applies to all five: the call is server-side, always

Your API key is the username you chose at registration. It is a server-side secret and it must never appear in client-side JavaScript, in a bundled configuration file, in page source, or in a mobile binary. If a browser needs a verdict, put a thin endpoint of your own in front of it and rate-limit that endpoint yourself — otherwise the key ends up in someone's devtools and the 10-requests-per-second budget belongs to whoever found it. The same rule applies to any check you expose through your public API to your own customers: see API security for the broader pattern.

Abuse at the front door

Signup screening, disposable domains, and the blast radius of one tenant

Two problems that look similar and are not: keeping bad accounts out, and limiting what a compromised good account can reach.

Signup abuse is where most platforms first go looking for domain intelligence, so be clear about what this dataset is: a list of hostnames verified as currently-live phishing and malware infrastructure — not a disposable-mail-provider list, not a domain-age service, not a reputation score, and not a bulk-registration detector, which means a throwaway address on a mailbox provider that exists to be throwaway will almost certainly not be flagged, because that provider is not phishing infrastructure.

At signup

Not merely disposable, but actively hostile

Accounts created from a domain that is simultaneously serving a credential-harvesting page are not marginal users to be nudged into verification — they are part of an operation, and the correct response is usually to refuse the signup outright, keep the record, and let your abuse team look at what else shares that infrastructure. Treat it as one input into a risk decision that also weighs age, reputation, disposable-provider lists and behavioural signals.

Blast radius

What a compromised workspace admin can reach

Tenant account takeover is a different problem in kind: it is about what a legitimate account can do once somebody else is driving it. In most multi-tenant products that list is uncomfortable to write down — invite new members, change or disable single sign-on enforcement, create API tokens, generate public share links for private content, alter webhook destinations, export data, and post content every member of that workspace sees attributed to a colleague they trust.

Internal trust

The best place in the world to post a phishing link

Nobody inspects a URL posted by their own team lead in their own project channel, so screening outbound links on write means the platform notices even when every human in the room has no reason to. It also gives incident response a concrete containment action: when an account is confirmed compromised, sweep every link it has posted through /batch and quarantine the matches, rather than asking a customer to re-read three weeks of their own history.

The mirror image

Somebody is building a copy of your sign-in screen

Generate the neighbouring candidates yourself — hyphenations, doubled letters, alternative top-level domains, app- and login- prefixes, your brand as a subdomain of a hosting platform — and run that list through /batch on a schedule, because one credit per hostname turns a suspicion into a verified answer you can take to a registrar. The brand protection page goes further into building and maintaining that candidate list.

Store the verdict, not just the decision. Persist is_phishing, category, confidence and last_checked against the record you screened. When a domain is later confirmed hostile, the thing you want is a query that finds every place it was ever accepted — and that only exists if you wrote the verdict down at the time.
Do not use this as your disposable-address list. It answers a different question. A clean result on a signup domain means "not confirmed phishing infrastructure", which is a long way from "this is a real company". Combine it with the signals you already trust rather than replacing them.
A clean result is not a safety guarantee. This is a known-bad lookup. A domain registered and first used within the same hour will not be on the list yet. Never render a "verified safe" badge on the strength of a 0.0 confidence response — the honest UI copy is that the link was not found on a known-threat list.
The latency budget

Do the arithmetic before you put a network call in a request path

Sub-50ms is excellent for an API and completely wrong as a dependency inside a page render. Those two statements are both true and the difference decides your architecture.

Start with the rate limit, because it is the number that settles most designs: ten requests per second per API key, with up to a hundred domains per /batch request, gives a ceiling of roughly a thousand domain verdicts per second per key — generous for a background job, an abuse sweep or a moderate signup flow, and nowhere near enough for an inline check on every content render in a product serving meaningful traffic, which no amount of clever retry logic changes.

What happens to a request that waits

Even at sub-50ms, adding a synchronous third-party call to a path a user is watching means accepting somebody else's availability as a component of your own: if the call is slow your page is slow, and if it fails you must decide in a hurry whether to block the write or let it through, with either choice wrong in some incident. Products that put a security lookup inline and then meet an outage usually end up disabling it permanently.

The pattern that works is boring

Accept the write immediately and mark the content unscreened, push the hostnames onto a queue, let a worker drain it calling /batch a hundred domains at a time, and update the record with the verdict. Render logic reads the stored verdict, never the network, and unscreened content renders with links inert for the seconds it takes the worker to catch up — so your throughput is bounded by your queue rather than by our rate limit.

Above a threshold, stop calling anything

The /feed endpoint returns the entire database as CSV with domain,category,dns_status or as JSON, with unlimited downloads on a subscription. A few hundred thousand hostnames fits comfortably in a hash set inside each application node, or a Bloom filter in front of one for a flatter memory profile, so lookups cost a memory access and the check becomes cheap enough to run on every render rather than once on write.

Refreshing is the only ongoing work

The build lands at 04:30 UTC with a changelog of domains added and removed, so a node applies a delta rather than reloading a full file — which matters when you would rather not have every instance in a large fleet pull and re-parse the whole database on the same schedule. Keep one alert on the age of the loaded data, because a silently stale set is the realistic failure mode and it is invisible unless you measure it.

Two jobs, two shapes

Screening other people's content, and screening on your own behalf

They share a dataset and almost nothing else. Building them as one system is the most common way this goes wrong.

Inbound: content your tenants create

High volume, unpredictable, adversarial, and latency-sensitive because it sits in product code paths, so the right shape is asynchronous screening on write with stored verdicts, backed by the feed in memory once volume justifies it, and failures here should degrade to "not yet screened" rather than to a blocked write. The consumer of the result is your rendering layer and your abuse tooling — nobody reads it by hand, so the output has to be a field on a record rather than a report.

Outbound: hostnames that imitate you

Low volume, generated by you rather than by users, and entirely tolerant of latency: a scheduled job builds a candidate list of hostnames adjacent to your own and runs it through /batch at one lookup each, weekly or daily depending on how aggressively you are targeted. The consumer here is a human on your trust and safety or security team who needs enough evidence to file a takedown, so the output should be a short list with dates, not a dashboard nobody opens.

Rule of thumb: if the result is read by code, it belongs in a queue and a database column. If the result is read by a person, it belongs in a scheduled job that produces a list. Products that build one system for both end up with a moderation queue full of machine-generated noise and a rendering path that waits on a report.
The endpoints you forgot were inputs

Webhooks, callback URLs and the redirect parameter nobody owns

Three features almost every SaaS product ships, all of which accept a destination from a user and all of which are routinely repurposed.

Open redirection is the quietest of these problems and the one with your domain most clearly attached: any endpoint that takes a URL or path in a query parameter and sends the browser there — the ?next= after login, the ?return_to= after an OAuth handshake, the "continue to your workspace" link in an onboarding email, the click-tracking hop in your marketing sends — is a redirector, and if it accepts absolute URLs to arbitrary hosts, an attacker can build a link that begins with your domain, passes a cursory glance and any filter that allow-lists you, and lands the victim somewhere else entirely.

Redirects

Allow-list first, screen second

An allow-list of permitted destinations should be your first move because it removes the class rather than filtering it. Where product requirements genuinely need arbitrary destinations — a link-in-bio product, a click tracker, a marketplace pointing at partner sites — screen the destination hostname before issuing the redirect, check again before redirecting if the stored value could have gone bad, and prefer an interstitial over a silent hop. The URL shorteners page covers redirect chains in more depth.

Webhooks

The same input wearing developer clothing

A tenant tells you where to send their events and your servers then make outbound requests to that address on a schedule you do not control. The literature on this is mostly about server-side request forgery and internal address ranges, which is correct and insufficient: a webhook destination is a hostname a tenant chose, and a tenant whose account has been taken over will happily point your event stream somewhere hostile. Screen at registration, re-screen periodically, and disable rather than silently drop.

Marketplace

OAuth redirect URIs are somebody else's URLs

An application listing in your marketplace is a set of addresses a third party controls, displayed to your customers with the implied endorsement of appearing in your product. Screening those on submission and re-screening on a schedule is cheap, and it catches the case where a legitimate integration's domain lapses and is re-registered by somebody who noticed before you did.

Abuse tooling

Three capabilities, and a case closes in minutes

Give your team an internal page where they can paste a hostname and get the verified verdict with dns_status and last_checked; a query listing every record in the product where a given hostname appears; and a bulk action that takes an account identifier and sweeps every link it ever posted through /batch at a hundred hostnames per request. Teams ingesting this into an existing detection stack should look at the threat intelligence patterns.

Tenant writes content
Accepted immediately
Hostnames queued
Worker batches 100
Verdict stored

Nothing in the write path waits on a network call. Render reads the stored verdict; content not yet screened renders with links inert rather than blocked.

From engineering and trust & safety

The questions that come up in the design review

Including the ones where the answer is "this does not do that".

Can we call this inline on every request?

You can, and at low volume it works fine, but you should not design for it. The rate limit is ten requests per second per API key — with /batch carrying up to a hundred domains, that is around a thousand verdicts per second per key, which a busy product will exceed. More importantly, an inline call makes a third party's availability part of your request path, and the first time that goes badly somebody will disable the check permanently. Screen asynchronously and store the verdict. If you genuinely need per-render checking, take the feed and hold the list in memory on each node — that removes the network from the path entirely and makes the rate limit irrelevant.

Will this catch disposable email domains at signup?

Generally no, and it is important not to buy this expecting that. The database contains hostnames verified as active phishing and malware infrastructure. A throwaway mailbox provider is neither of those things — it is an unwanted signup source, which is a different problem with its own dedicated lists. What it catches at signup is the narrower and more serious case: an account being created from a domain that is simultaneously running a credential-harvesting page. That is a strong enough signal to refuse the registration and investigate what else shares the infrastructure. Use it alongside your disposable-provider list and your behavioural signals, not instead of them.

What does a clean response actually mean, and can we show users a "safe" badge?

A clean response means the hostname was not found in a database of DNS-verified active phishing domains at the moment you asked. It does not mean the destination is safe, and you should not build UI that says it is. The honest copy is something like "not found on a known-threat list", and the honest visual treatment is the absence of a warning rather than the presence of a reassurance. The confidence field makes this unusually easy to reason about: 0.98 for a confirmed match and 0.0 for no match, with nothing in between. There is no ambiguous middle band to translate into a colour, which means your UI has exactly two states to design rather than a spectrum to invent.

How do we handle a domain that becomes malicious after we accepted it?

This is the case that makes storing verdicts worthwhile rather than optional. Because you persisted the hostname and the date you checked it, you can re-screen retrospectively: take the daily changelog of added domains, intersect it with the distinct hostnames in your content tables, and you have a precise list of records that need re-evaluating. That intersection is cheap because it runs against data you already hold. What you do with the matches is a product decision. Quarantining the content and notifying the workspace owner is usually right; silently deleting other people's data usually is not. Either way the operation should be an existing bulk action in your abuse tooling rather than a script somebody writes during an incident.

We are a small team. Is the feed subscription overkill compared with credits?

Almost certainly, at first. Credits are monthly subscriptions via PayPal and billed monthly, starting at Growth at $99/month for 25,000 lookups, Growth at $99/month for 25,000 lookups , and Professional at $249/month for 100,000 lookups with priority support. A product screening signups, invites and content on write will spend a long time inside those numbers, and the fourteen-day refund window applies if under ten percent has been used. The Daily Threat Feed at $499 per month becomes the better economics when your check volume passes the point where per-lookup pricing dominates, or when you want the list in memory for latency reasons. all plans include priority support. Feed downloads are unlimited, so a fleet of two nodes and a fleet of two hundred cost the same. The full table is on the pricing page.

Our customers ask about our security posture. Can we tell them we do this?

You can, as long as you describe it accurately. A defensible sentence for a security questionnaire is: hostnames in user-generated content, signup domains and webhook destinations are screened against a third-party database of DNS-verified active phishing domains, rebuilt every twenty-four hours, with verdicts stored and retrospectively re-evaluated against the daily changelog. What you should not claim is that links in your product are verified safe, or that phishing cannot be distributed through your platform. Both are false, and an enterprise reviewer with any experience will find the gap. Understating a real control reads as competence; overstating one reads as a warning sign.

Does this help with a compromised tenant admin, or only with external links?

Both, but by different mechanisms. For links, screening on write means content posted by a compromised account is evaluated even though every human in that workspace has good reason to trust the sender — internal trust is exactly what makes the takeover valuable, and the platform is the only party in the room that is not fooled. For the takeover itself, this reduces the arrival rate rather than detecting the compromise. If the credentials were harvested through a look-alike sign-in page, screening your own neighbouring hostnames on a schedule is how you find that page and get it removed. Detecting the compromised session is a different problem that belongs to your authentication and anomaly detection, and nothing here substitutes for enforcing multi-factor authentication on workspace administrators.

Can we expose this check to our own customers as a feature?

Yes, and several platforms do — a safe-link indicator, an admin-facing report of flagged content, or an API of your own that surfaces the verdict. The constraint is that your key stays on your servers and your endpoint carries its own authentication and rate limiting. Do not proxy raw, unauthenticated access to the check from a browser; that is functionally publishing your key. Also think about what happens when a customer disagrees with a verdict. Build the override path before you ship the feature, keep it per-workspace rather than global, and log who exercised it. A feature that flags a customer's own marketing domain with no way to appeal generates more support load than the feature saves.

Put the check where the hostname crosses your boundary

Signup, invite, profile, share and render — screened asynchronously, stored as a verdict, re-evaluated against the daily changelog. Start against the documented endpoints, and move to the in-memory feed when the latency budget says so.