A close-up portrait of a man with a salt-and-pepper beard wearing a white collared shirt against a textured beige background.
A close-up portrait of a man with a salt-and-pepper beard wearing a white collared shirt against a textured beige background.

Invitations need notifications

Jesse James Richard
|
Feb 26, 2026
|
7 min read
#Access Control
#Architecture

An owner invites a teammate to their organization. The teammate has no account yet, so there is no way to reach them inside the app, and reaching them outside it means sending email.

Sending email is a system, not a function call.

Why invitations exist at all

Giant Context is multitenant. A customer is an organization, and organizations are sealed off from each other completely. People are not, because real work crosses those boundaries. A freelance designer builds sites for six different companies. An agency runs twenty client accounts. A consultant drops into a project for a month and leaves. Requiring a new account for every organization a person touches would describe a way of working that nobody has.

So the model is one person, many memberships. You are one identity on the platform, and you are a member of every organization that has invited you, with a possibly different role in each. An owner in your own company, an editor in a client's, a viewer in a third.

The invitation adds a membership without creating a person. It has to work for someone who has no account yet, and for someone who already has one and is joining another organization.

The click

An owner opens their members list, enters a teammate's email, picks the role that teammate will hold, and sends. On my server, that creates an invitation record with a securely generated random token and an expiry twenty-four hours out, and the token is the whole security model. It is a secret long enough that nobody guesses it, carried in a link, and it is the only key that opens this particular invitation.

A dark-themed user interface modal titled 'Invite Member' with fields for email address, organization role, and project memberships.
The invite dialog. An email, a role, and a send.

That invitation now has to reach an inbox.

The notifications layer underneath

You can run your own outbound mail server. People do. What you take on with it is deliverability, which is the work of making inboxes accept your mail: reputation on a new sending IP, SPF and DKIM and DMARC records, bounce handling, complaint feedback loops, and the slow warming of a domain nobody has seen before. Get any of it wrong and your invitations land in spam, which looks identical to your invitations not being sent.

That is a job. It is not the job I am doing, so outbound mail goes through a transactional email service that has already solved it. SendGrid, Mailgun, Postmark, Amazon SES and Resend all do this. Any of them is a better sender than I would be.

I would rather it were Google, because everything else here runs on Google Cloud and one provider means one bill, one identity model, one place to look during an incident. Google Cloud has no transactional email service. So this is the one dependency sitting outside the platform's cloud, and it is a deliberate exception rather than an oversight.

Which vendor it is should not be a fact the product knows. Invitations are one of a dozen things the platform sends. Password resets, billing warnings, notifications. Wiring each of them to a vendor directly means changing a dozen features to change one vendor, and email is only one channel anyway: a notification can also reach a device or a badge inside the app.

So the invitation hands a message to a notifications system, and behind that sits one interface:

The provider interface

export interface EmailProvider {  send(options: EmailOptions): Promise<EmailResult>;}

Behind that interface sits whichever provider the environment chooses. In production, the transactional service. Locally, MailHog, a fake mail server that catches everything the app sends and shows it in a web inbox.

MailHog is how the flow got proved. I clicked invite, opened the web inbox, and there was the real email with the real link, which I clicked and accepted, and no message left my machine. It is a permanent fixture of the dev environment, because the alternative is sending test invitations to my own address all day.

The invitation code names none of these providers. It calls the notifications system, and the notifications system resolves the rest.

Who can accept it

The teammate gets an email with a link carrying the token, and clicks it. Two people can arrive at that link, and both have to work.

Someone with no account signs up, and the token is spent afterwards. The identity comes first, the membership second.

Someone who already has an account, from another organization they belong to, is added to this one. No second identity, no second password. The same person, holding one more membership.

Both arrivals end at the same check. The email on the accepting account must match the email the invitation was sent to. An invitation to maria@agency.com can only be accepted by the account that owns maria@agency.com, which closes the hole a forwarded link would otherwise open. An invite gets one person into one organization, and only the person it names.

A screenshot of a dark-themed web application interface showing an Invitations tab within a settings menu.
A pending invitation in the members list, waiting on the person it names.

The membership is written

The token is spent, the membership row is written, and the new member lands in the organization holding exactly the role the owner picked. Their next request crosses the same permission gates as everyone else, and now those gates know them. In the members list, the pending row becomes a person.

Wiring this up yourself

If you are building multitenant software, the invitation is the foundation of your membership model, so build it early and build it as a token flow.

Make the token the security model. A long random secret in a link, a short expiry, single use. Verify by the email the accepter proves they own rather than by anything the sender controls.

Handle both arrivals from the first line of code. Someone signing up for the first time, and someone who already has an account joining an additional organization. A flow that only handles new signups puts a wall in front of every freelancer and agency, because those are the people who join a second organization.

And when the invitation needs to email someone, build the notifications layer before wiring anything to a vendor. Provider behind an interface, invite routed through it. Password resets, billing warnings and in-app notifications all arrive later and all need the same path, and adding them to an interface that already exists is cheaper than retrofitting one around a hard-coded vendor call.

Connecting a custom domain

#Architecture

This site is the case. jessejamesrichard.com is registered elsewhere, built in the console, hosted on Giant Context, and served over HTTPS on its own ...

Jesse James Richard

|

Feb 23, 2026
Read previous

Building something like this

I'm Jesse. I build platforms end to end, and I'm open to work. If this is the kind of engineering you need, get in touch.

Contact Jesse
Home
About
Contact
Sitemap
Privacy Policy
Terms of Service
Cookie Policy
Invitations need notifications | Jesse James Richard